Index: /tags/rel-0_0_1/archive/psdb/makefile
===================================================================
--- /tags/rel-0_0_1/archive/psdb/makefile	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/makefile	(revision 7462)
@@ -0,0 +1,29 @@
+SHELL=/bin/sh
+CFG=`which mysql_config`
+CFLAGS=-g -O0 -pipe -fpic -Wall -pedantic -std=c99
+MYSQL_LIBS:=$(shell $(CFG) --libs)
+MYSQL_FLAGS:=$(shell $(CFG) --cflags)
+LIBS=-lpslib
+
+test_objects=test.o psleak.o
+objects=psDB.o
+
+all: libpsdb.so
+
+test: test-bin
+	LD_LIBRARY_PATH=. ./test
+     
+test-bin: $(test_objects) libpsdb.so
+	$(CC) $(CFLAGS) -o test $(test_objects) -L./ -lpsdb
+
+$(test-objects): %.o : %.c
+	$(CC) $(CFLAGS) -o $@ -c $<
+
+libpsdb.so: $(objects)
+	$(CC) $(CFLAGS) -shared -o libpsdb.so $(objects) $(LIBS) $(MYSQL_FLAGS) $(MYSQL_LIBS)
+
+$(objects): %.o : %.c
+	$(CC) $(CFLAGS) -o $@ -c $<
+
+clean:
+	$(RM) *.so *.o core test
Index: /tags/rel-0_0_1/archive/psdb/psDB.c
===================================================================
--- /tags/rel-0_0_1/archive/psdb/psDB.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/psDB.c	(revision 7462)
@@ -0,0 +1,1654 @@
+/** @file  psDB.c
+ *
+ *  @brief database functions
+ *
+ *  This file contains functions that perform basic database operations.  MySQL
+ *  4.1.2 or newer is required.
+ *
+ *  @author Joshua Hoblitt
+ *
+ *  @version $Revision: 1.34 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-02-17 03:22:06 $
+ *
+ *  Copyright 2005 Joshua Hoblitt, University of Hawaii
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <math.h>
+#include <mysql/mysql.h>
+#include <mysql/mysql_com.h> // enum_field_types
+
+#include "psDB.h"
+#include "pslib.h"
+
+typedef struct
+{
+    enum enum_field_types type;
+    bool            isUnsigned;
+} mysqlType;
+
+// database utility functions
+static bool     psDBRunQuery(psDB *dbh, const char *query);
+static inline bool psDBPackRow(MYSQL_BIND *bind, psMetadata *values, psU32 paramCount);
+
+// SQL generation functions
+static char    *psDBGenerateCreateTableSQL(const char *tableName, psMetadata *where);
+static char    *psDBGenerateSelectRowSQL(const char *tableName, const char *col, psMetadata *where, psU64 limit);
+static char    *psDBGenerateInsertRowSQL(const char *tableName, psMetadata *row);
+static char    *psDBGenerateUpdateRowSQL(const char *tableName, psMetadata *where, psMetadata *values);
+static char    *psDBGenerateDeleteRowSQL(const char *tableName, psMetadata *where);
+static char    *psDBGenerateWhereSQL(psMetadata *where);
+static char    *psDBGenerateSetSQL(psMetadata *set);
+
+// lookup table functions
+static psElemType psDBMySQLToPType(enum enum_field_types type, unsigned int flags);
+static char    *psDBPTypeToSQL(psElemType pType);
+static mysqlType *psDBPTypeToMySQL(psElemType pType);
+
+static psHash  *psDBGetPTypeToSQLTable(void);
+static void     psDBPTypeToSQLTableCleanup(void);
+
+static psHash  *psDBGetSQLToPTypeTable(void);
+static void     psDBSQLToPTypeTableCleanup(void);
+
+static psHash  *psDBGetMySQLToSQLTable(void);
+static void     psDBMySQLToSQLTableCleanup(void);
+
+static psHash  *psDBGetPTypeToMySQLTable(void);
+static void     psDBPTypeToMySQLTableCleanup(void);
+
+static psPtr    psDBMySQLTypeAlloc(enum enum_field_types type, bool isUnsigned);
+static void     psDBAddToLookupTable(psHash *lookupTable, psU32 type, const char *string);
+static void     psDBAddVoidToLookupTable(psHash *lookupTable, psU32 type, psPtr value);
+
+// pType utility functions
+static psPtr    psDBGetPTypeNaN(psElemType pType);
+static bool     psDBIsPTypeNaN(psElemType pType, psPtr data);
+
+// string utility functions
+static char    *psDBIntToString(psU64 n);
+static ssize_t  psStringAppend(char **dest, const char *format, ...);
+static ssize_t  psStringPrepend(char **dest, const char *format, ...);
+
+
+// public functions
+/*****************************************************************************/
+
+psDB *psDBInit(const char *host, const char *user, const char *passwd, const char *dbname)
+{
+    MYSQL           *mysql;
+    psDB            *dbh;
+
+    mysql = mysql_init(NULL);
+    if (!mysql) {
+         psAbort(__func__, "mysql_init(), out of memory.");
+    }
+
+    if (!mysql_real_connect(mysql, host, user, passwd, dbname, 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to connect to database.  Error: %s", mysql_error(mysql));
+
+        mysql_close(mysql);
+
+        return NULL;
+    }
+
+    dbh = psAlloc(sizeof(psDB));
+
+    dbh->mysql = mysql;
+
+    return dbh;
+}
+
+void psDBCleanup(psDB *dbh)
+{
+    mysql_close(dbh->mysql);
+    psFree(dbh);
+
+    // these lookup tables should probably be moved into psDB to prevent them
+    // from being flushed in the case that a database handle is closed while
+    // one or more are still open
+    psDBMySQLToSQLTableCleanup();
+    psDBPTypeToSQLTableCleanup();
+    psDBSQLToPTypeTableCleanup();
+    psDBPTypeToMySQLTableCleanup();
+}
+
+bool psDBCreate(psDB *dbh, const char *dbname)
+{
+    char            *query = NULL;
+    bool            status;
+    
+    psStringAppend(&query, "CREATE DATABASE %s", dbname);
+
+    // the MySQL C API notes that mysql_create_db() is deprecated
+    status = psDBRunQuery(dbh, query);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to create new database.");
+    }
+
+    psFree(query);
+
+    return status;
+}
+
+bool psDBChange(psDB *dbh, const char *dbname)
+{
+    if (mysql_select_db(dbh->mysql, dbname) != 0) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to change database.  Error: %s", mysql_error(dbh->mysql));
+           
+        return false;
+    }
+
+    return true;
+}
+
+bool psDBDrop(psDB *dbh, const char *dbname)
+{
+    char            *query = NULL;
+    bool            status;
+    
+    psStringAppend(&query, "DROP DATABASE %s", dbname);
+    
+    // the MySQL C API notes that mysql_drop_db() is deprecated
+    status = psDBRunQuery(dbh, query);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to drop database.");
+    }
+
+    psFree(query);
+
+    return status;
+}
+
+bool psDBCreateTable(psDB *dbh, const char *tableName, psMetadata *md)
+{
+    char            *query;
+    bool            status;
+
+    query = psDBGenerateCreateTableSQL(tableName, md);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return NULL;
+    }
+
+    status = psDBRunQuery(dbh, query);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to create table.");
+    }
+
+    psFree(query);
+
+    return status;
+}
+
+bool psDBDropTable(psDB *dbh, const char *tableName)
+{
+    char            *query = NULL;
+    bool            status;
+
+    psStringAppend(&query, "DROP TABLE %s", tableName);
+
+    status = psDBRunQuery(dbh, query);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "Failed to drop table.");
+    }
+
+    psFree(query);
+
+    return status;
+}
+
+psArray *psDBSelectColumn(psDB *dbh, const char *tableName, const char *col, psU64 limit)
+{
+    MYSQL_RES       *result;            // complete db result set
+    MYSQL_ROW       row;                // single row of db result set
+    char            *query;             // SQL query
+    psU64           rowCount;           // number of rows in db result set
+    psU64           dataSize;           // size of field
+    unsigned int    fieldCount;         // number of fields in db result set
+    psArray         *column = NULL;     // return array
+    psPtr           data;               // copy of result field
+
+    query = psDBGenerateSelectRowSQL(tableName, col, NULL, limit);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return NULL;
+    }
+
+    if (!psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "Query execution failed.");
+
+        psFree(query);
+
+        return NULL;
+    }
+
+    psFree(query);
+
+    result = mysql_store_result(dbh->mysql);
+    if (!result) {
+        // no result set
+        fieldCount = mysql_field_count(dbh->mysql);
+
+        // if field count is zero the query returned no data.  If it's non-zero
+        // then something bad has happened.
+        if (fieldCount != 0) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Query returned no data.  Error: %s", mysql_error(dbh->mysql));
+
+            return NULL;
+        }
+    }
+
+    rowCount = (psU64)mysql_num_rows(result);
+
+    // pre-allocate enough elements to hold the complete result set
+    // then reset n to 0 so elements are added from the beginning of
+    // the array
+    column = psArrayAlloc(rowCount);
+    column->n = 0;
+
+    while ((row = mysql_fetch_row(result))) {
+        // get the first element of lengths array that is part of the
+        // result set
+        dataSize = (psU64)*mysql_fetch_lengths(result);
+
+        // represent NULL as an empty string
+        if (row[0] == NULL) {
+            data = psStringCopy("");
+        } else {
+            data = psStringNCopy(row[0], dataSize);
+        }
+
+        // add field to return array
+        psArrayAdd(column, 0, data);
+    }
+
+    mysql_free_result(result);
+
+    return column;
+}
+
+// dest = assign too, source = source string psArray, conv = conversion function,
+// type = type to cast to, pType = psElemType
+#define PS_STR_ARRAY_TO_PTYPE(dest, source, conv, type, pType) \
+{ \
+    psPtr           myNaN; \
+    int             i; \
+\
+    for (i = 0; i < source->n; i++) { \
+        if (strlen(source->data[i])) { \
+            dest[i] = (type)conv(source->data[i]); \
+        } else { \
+            myNaN = psDBGetPTypeNaN(pType); \
+            dest[i] = *(type *)myNaN; \
+            psFree(myNaN); \
+        } \
+    } \
+}
+
+psVector *psDBSelectColumnNum(psDB *dbh, const char *tableName, const char *col, psElemType pType, const psU64 limit)
+{
+    psArray         *stringColumn;      // source psArray
+    psVector        *column;            // dest psVector
+
+    stringColumn = psDBSelectColumn(dbh, tableName, col, limit);
+    if (!stringColumn) {
+        // could be an error or the result set was just empty
+        return NULL;
+    }
+
+    column = psVectorAlloc(stringColumn->n, pType);
+
+    // conversion functions are a portability issue
+    switch (pType) {
+        case PS_TYPE_S8:
+            PS_STR_ARRAY_TO_PTYPE(column->data.S8, stringColumn, atoi, psS8, PS_TYPE_S8);
+            break;
+        case PS_TYPE_S16:
+            PS_STR_ARRAY_TO_PTYPE(column->data.S16, stringColumn, atoi, psS16, PS_TYPE_S16);
+            break;
+        case PS_TYPE_S32:
+            PS_STR_ARRAY_TO_PTYPE(column->data.S32, stringColumn, atoi, psS32, PS_TYPE_S32);
+            break;
+        case PS_TYPE_S64:
+            PS_STR_ARRAY_TO_PTYPE(column->data.S64, stringColumn, atoll, psS64, PS_TYPE_S64);
+            break;
+        case PS_TYPE_U8:
+            PS_STR_ARRAY_TO_PTYPE(column->data.U8, stringColumn, atoi, psU8, PS_TYPE_U8);
+            break;
+        case PS_TYPE_U16:
+            PS_STR_ARRAY_TO_PTYPE(column->data.U16, stringColumn, atoi, psU16, PS_TYPE_U16);
+            break;
+        case PS_TYPE_U32:
+            PS_STR_ARRAY_TO_PTYPE(column->data.U32, stringColumn, atoi, psU32, PS_TYPE_U32);
+            break;
+        case PS_TYPE_U64:
+            PS_STR_ARRAY_TO_PTYPE(column->data.U64, stringColumn, atoll, psU64, PS_TYPE_U64);
+            break;
+        case PS_TYPE_F32:
+            PS_STR_ARRAY_TO_PTYPE(column->data.F32, stringColumn, atof, psF32, PS_TYPE_F32);
+            break;
+        case PS_TYPE_F64:
+            PS_STR_ARRAY_TO_PTYPE(column->data.F64, stringColumn, atof, psF64, PS_TYPE_F64);
+            break;
+        case PS_TYPE_C32:
+            // this is a bogus SQL type
+            PS_STR_ARRAY_TO_PTYPE(column->data.C32, stringColumn, atof, psC32, PS_TYPE_C32);
+            break;
+        case PS_TYPE_C64:
+            // this is a bogus SQL type
+            PS_STR_ARRAY_TO_PTYPE(column->data.C64, stringColumn, atof, psC64, PS_TYPE_C64);
+            break;
+        case PS_TYPE_BOOL:
+            // valid for psVector?
+            break;
+        case PS_TYPE_PTR:
+            // valid for psVector?
+            break;
+    }
+
+    psFree(stringColumn);
+
+    return column;
+}
+
+psArray *psDBSelectRows(psDB *dbh, const char *tableName, psMetadata *where, const psU64 limit)
+{
+    MYSQL_RES       *result;            // complete db result set
+    MYSQL_ROW       row;                // single row of db result set
+    MYSQL_FIELD     *field;             // field type info
+    char            *query;             // SQL query
+    psU64           rowCount;           // number of rows in db result set
+    psU64           fieldCount;         // number of fields in db result set
+    psU64           *fieldLength;       // field sizes
+    psArray         *resultSet;         // return array
+    int             i;                  // field index
+    psMetadata      *md;                // a row
+    psU32           pType;              // psElemType of a field
+    psU32           type;               // psMetadataType of a a field
+    psPtr           data;               // copy of result field
+
+    query = psDBGenerateSelectRowSQL(tableName, NULL, where, limit);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return NULL;
+    }
+
+    if (!psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "Query execution failed.");
+
+        psFree(query);
+
+        return NULL;
+    }
+
+    psFree(query);
+
+    result = mysql_store_result(dbh->mysql);
+    if (!result) {
+        // no result set
+        fieldCount = mysql_field_count(dbh->mysql);
+
+        // if field count is zero the query should have returned no data.  If
+        // it's non-zero then something bad has happened.
+        if (fieldCount != 0) {
+            psError(PS_ERR_UNEXPECTED_NULL, true, "Query returned no data.  Error: %s", mysql_error(dbh->mysql));
+
+            return NULL;
+        }
+    }
+
+    rowCount = (psU64)mysql_num_rows(result);
+
+    // pre-allocate enough elements to hold the complete result set
+    // then reset n to 0 so elements are added from the beginning of
+    // the array
+    resultSet = psArrayAlloc(rowCount);
+    resultSet->n = 0;
+
+    field = mysql_fetch_fields(result);
+    fieldCount = mysql_num_fields(result);
+
+    while ((row = mysql_fetch_row(result))) {
+        // allocate new psMetadata to represent a row
+        md = psMetadataAlloc();
+
+        fieldLength = (psU64 *)mysql_fetch_lengths(result);
+
+        for (i = 0; i < fieldCount; i++) {
+            // lookup MySQL column type
+            pType = psDBMySQLToPType(field[i].type, field[i].flags);
+
+            // copy field data and convert NULLs to the appropriate NaN value
+            if (pType == PS_TYPE_PTR) {
+                type = PS_META_STR;
+                data = fieldLength[i] ? psStringNCopy(row[i], fieldLength[i])
+                                      : psDBGetPTypeNaN(pType);
+            } else {
+                type = PS_META_PRIMITIVE;
+                if (fieldLength[i]) {
+                    data = psAlloc(fieldLength[i]);
+                    memcpy(data, row[i], fieldLength[i]);
+                } else {
+                    data = psDBGetPTypeNaN(pType);
+                }
+            }
+
+            // add new field to row
+            psMetadataAdd(md, i, field[i].name, pType, type, NULL, data);
+
+            psFree(data);
+        }
+
+        // add row to result set
+        psArrayAdd(resultSet, 0, md);
+    }
+
+    mysql_free_result(result);
+
+    return resultSet;
+}
+
+bool psDBInsertOneRow(psDB *dbh, const char *tableName, psMetadata *row)
+{
+    psArray         *rowSet;            // psArray of row to insert
+
+    // this is to prevent row from being free'd when rowSet is free'd. this
+    // will have to be removed when psArrayAdd is modified to do this for you.
+    psMemIncrRefCounter(row);
+
+    rowSet = psArrayAlloc(1);
+    rowSet->n = 0;
+    psArrayAdd(rowSet, 0, row);
+
+    if (!psDBInsertRows(dbh, tableName, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "Insert failed.");
+
+        psFree(rowSet);
+
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool psDBInsertRows(psDB *dbh, const char *tableName, psArray *rowSet)
+{
+    psMetadata      *row;               // row of data
+    char            *query;             // SQL query
+    MYSQL_STMT      *stmt;              // prepared db statement
+    MYSQL_BIND      *bind;              // field values to insert
+    psU32           paramCount;         // number of placeholders in query
+    psU64           j;                  // row index
+
+    // we are assuming that all rows in the set have an identical with reguard
+    // to field count and type
+    row = rowSet->data[0];
+
+    query = psDBGenerateInsertRowSQL(tableName, row);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return NULL;
+    }
+
+    stmt = mysql_stmt_init(dbh->mysql);
+    if (!stmt) {
+        psAbort(__func__, "mysql_stmt_init(), out of memory.");
+    }
+
+    if (mysql_stmt_prepare(stmt, query, (unsigned long)strlen(query))) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to prepare query.  Error: %s", mysql_stmt_error(stmt));
+
+        mysql_stmt_close(stmt);
+        psFree(query);
+
+        return false;
+    }
+
+    psFree(query);
+
+    // how many place holders are in our query
+    paramCount = (psU32)mysql_stmt_param_count(stmt);
+
+    // structure larger enough to hold one field of data per place holder
+    bind = psAlloc(sizeof(MYSQL_BIND) * paramCount);
+
+    // loop over rows
+    for (j = 0; j < rowSet->n; j++) {
+        row = rowSet->data[j];
+
+        // reset bind for each row
+        memset(bind, 0, sizeof(MYSQL_BIND) * paramCount);
+
+        if (!psDBPackRow(bind, row, paramCount)) {
+            psError(PS_ERR_UNKNOWN, false, "Failed to pack params into bind structure.");
+
+            mysql_rollback(dbh->mysql);
+
+            psFree(bind);
+            mysql_stmt_close(stmt);
+        }
+
+        if (mysql_stmt_bind_param(stmt, bind)) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to bind params.  Error: %s", mysql_stmt_error(stmt));
+
+            mysql_rollback(dbh->mysql);
+
+            psFree(bind);
+            mysql_stmt_close(stmt);
+
+            return false;
+        }
+
+        if (mysql_stmt_execute(stmt)) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to execute prepared statement.  Error: %s",
+                mysql_stmt_error(stmt));
+
+            mysql_rollback(dbh->mysql);
+
+            psFree(bind);
+            mysql_stmt_close(stmt);
+
+            return false;
+        }
+    } // end loop over rows
+
+    // point of no return
+    mysql_commit(dbh->mysql);
+
+    psFree(bind);
+    mysql_stmt_close(stmt);
+
+    return true;
+}
+
+psArray *psDBDumpRows(psDB *dbh, const char *tableName)
+{
+    return psDBSelectRows(dbh, tableName, NULL, 0);
+}
+
+psMetadata *psDBDumpCols(psDB *dbh, const char *tableName)
+{
+    MYSQL_RES       *result;
+    MYSQL_FIELD     *field;
+    psU32           fieldCount;
+    psMetadata      *table;
+    psU32           pType;
+    psU32           i;
+    psPtr           column;
+
+    // find column types
+    result = mysql_list_fields(dbh->mysql, tableName, NULL);
+    if (!result) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Failed to retrieve column types."); 
+    }
+
+    field = mysql_fetch_fields(result);
+    fieldCount = mysql_num_fields(result);
+
+    table = psMetadataAlloc();
+
+    // fetch each column and load into psMetadata
+    for (i =0; i < fieldCount; i++) {
+        // find ptype of column
+        pType = psDBMySQLToPType(field[i].type, field[i].flags);
+        
+        // if the ptype is PS_TYPE_PTR assume that it's a string and fetch the
+        // column as an psArray of strings; otherwise fetch the column as a
+        // psVector.  
+        if (pType == PS_TYPE_PTR) {
+            // PS_META_UNKNOWN -> PS_META_ARRAY ?
+            column = psDBSelectColumn(dbh, tableName, field[i].name, 0);
+            psMetadataAdd(table, 0, field[i].name, PS_TYPE_PTR, PS_META_UNKNOWN, "psArray", column);
+            // FIXME
+            //psFree(column);
+        } else {
+            column = psDBSelectColumnNum(dbh, tableName, field[i].name, pType, 0);
+            psMetadataAdd(table, 0, field[i].name, PS_TYPE_PTR, PS_META_VEC, "psVector", column);
+            // FIXME
+            //psFree(column);
+        }
+    }
+    
+    return table;
+}
+
+psS64 psDBUpdateRows(psDB *dbh, const char *tableName, psMetadata *where, psMetadata *values)
+{
+    char            *query;
+    MYSQL_STMT      *stmt;              // prepared db statement
+    psU32           paramCount;         // number of placeholders in query
+    MYSQL_BIND      *bind;              // field values to insert
+    psU64           rowsAffected;       // number of rows affected by query
+
+    query = psDBGenerateUpdateRowSQL(tableName, where, values);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return -1;
+    }
+
+    stmt = mysql_stmt_init(dbh->mysql);
+    if (!stmt) {
+        psAbort(__func__, "mysql_stmt_init(), out of memory.");
+    }
+
+    if (mysql_stmt_prepare(stmt, query, (unsigned long)strlen(query))) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to prepare query.  Error: %s", mysql_stmt_error(stmt));
+
+        mysql_stmt_close(stmt);
+        psFree(query);
+
+        return -1;
+    }
+
+    psFree(query);
+
+    // how many place holders are in our query
+    paramCount = (psU32)mysql_stmt_param_count(stmt);
+
+    // structure large enough to hold one field of data per place holder
+    bind = psAlloc(sizeof(MYSQL_BIND) * paramCount);
+
+    // init bind
+    memset(bind, 0, sizeof(MYSQL_BIND) * paramCount);
+
+    if (!psDBPackRow(bind, values, paramCount)) {
+        psFree(bind);
+        mysql_stmt_close(stmt);
+
+        return -1;
+    }
+
+    if (mysql_stmt_bind_param(stmt, bind)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to bind params.  Error: %s", mysql_stmt_error(stmt));
+
+        mysql_rollback(dbh->mysql);
+
+        psFree(bind);
+        mysql_stmt_close(stmt);
+
+        return -1;
+    }
+
+    if (mysql_stmt_execute(stmt)) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to execute prepared statement.  Error: %s",
+            mysql_stmt_error(stmt));
+
+        mysql_rollback(dbh->mysql);
+
+        psFree(bind);
+        mysql_stmt_close(stmt);
+
+        return -1;
+    }
+
+    psFree(bind);
+
+    // point of no return
+    mysql_commit(dbh->mysql);
+
+    rowsAffected = (psU64)mysql_stmt_affected_rows(stmt);
+
+    mysql_stmt_close(stmt);
+
+    return rowsAffected;
+}
+
+psS64 psDBDeleteRows(psDB *dbh, const char *tableName, psMetadata *where)
+{
+    char            *query;
+
+    query = psDBGenerateDeleteRowSQL(tableName, where);
+    if (!query) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Query generation failed.");
+
+        return -1;
+    }
+
+    if (!psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "Delete failed.");
+
+        mysql_rollback(dbh->mysql);
+
+        psFree(query);
+
+        return -1;
+    }
+
+    psFree(query);
+
+    if (!where) {
+        // we truncated the table so mysql_affected_rows() won't work
+        return 1;
+    }
+
+    // point of no return
+    mysql_commit(dbh->mysql);
+
+    return (psS64)mysql_affected_rows(dbh->mysql);
+}
+
+// database utility functions
+/*****************************************************************************/
+
+static bool psDBRunQuery(psDB *dbh, const char *query)
+{
+    if (mysql_real_query(dbh->mysql, query, (unsigned long)strlen(query)) !=0) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to execute query.  Error: %s\n", mysql_error(dbh->mysql));
+           
+        return false;
+    }
+
+    return true;
+}
+
+static inline bool psDBPackRow(MYSQL_BIND *bind, psMetadata *values, psU32 paramCount)
+{
+    psListIterator  *cursor;            // row iterator
+    psMetadataItem  *item;              // field in row
+    mysqlType       *mType;             // type tmp variable
+    static bool     isNull = true;      // used in a MYSQL_BIND to indicate NULL,
+                                        // this will be used outside of this func
+    psU32           i;                  // field index
+
+    // check size of values == paramCount ?
+
+    cursor = psListIteratorAlloc(values->list, 0);
+
+    // loop over fields
+    i = 0;
+    while ((item = psListGetNext(cursor))) {
+        // lookup pType -> mysql type
+        mType = psDBPTypeToMySQL(item->pType);
+
+        bind[i].buffer_type = mType->type;
+        bind[i].is_unsigned = mType->isUnsigned;
+
+        psFree(mType);
+
+        // input data length is determined by the MYSQL_TYPE_* unless it's a string
+        if (item->type == PS_META_PRIMITIVE) {
+            switch (item->pType) {
+                case PS_TYPE_S32:
+                    bind[i].length  = 0;
+                    bind[i].buffer  = &item->data.S32;
+                    bind[i].is_null = psDBIsPTypeNaN(item->pType, &item->data.S32) 
+                                    ? (my_bool *)&isNull
+                                    : NULL;
+                    break;
+                case PS_TYPE_F32:
+                    bind[i].length  = 0;
+                    bind[i].buffer  = &item->data.F32;
+                    bind[i].is_null = psDBIsPTypeNaN(item->pType, &item->data.F32) 
+                                    ? (my_bool *)&isNull
+                                    : NULL;
+                    break;
+                case PS_TYPE_F64:
+                    bind[i].length  = 0;
+                    bind[i].buffer  = &item->data.F64;
+                    bind[i].is_null = psDBIsPTypeNaN(item->pType, &item->data.F64) 
+                                    ? (my_bool *)&isNull
+                                    : NULL;
+                    break;
+                default:
+                    psError(PS_ERR_BAD_PARAMETER_TYPE , true,
+            "FIXME: Only PS_META_PRIMITIVE values of PS_TYPE_S32, PS_TYPE_F32, and PS_TYPE_F64 supported.");
+                    break;
+            }
+        // convert NaNs to NULL and set the buffer_length for strings
+        } else if ((item->type == PS_META_STR) && (item->pType == PS_TYPE_PTR)) {
+            bind[i].buffer_length = (unsigned long)strlen((char *)item->data.V);
+            bind[i].length  = &bind[i].buffer_length;
+            bind[i].buffer  = item->data.V;
+            bind[i].is_null = *(char *)item->data.V == '\0' 
+                            ? (my_bool *)&isNull
+                            : NULL;
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_TYPE , true,
+                "Only types of PS_META_PRIMITIVE and PS_META_STR are supported.");
+
+            psFree(cursor);
+
+            return false;
+        }
+
+        // increment field index
+        i++;
+    }
+
+    psFree(cursor);
+
+    return true;
+}
+
+
+// SQL generation functions
+/*****************************************************************************/
+
+static char *psDBGenerateCreateTableSQL(const char *tableName, psMetadata *table)
+{
+    char            *query = NULL;      // complete query
+    psMetadataItem  *item;              // column description
+    psListIterator  *cursor;            // column iterator
+    char            *colType;           // type lookup table
+
+    if (!table) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, "table param may not be null.");
+
+        return NULL;
+    }
+
+    psStringAppend(&query, "CREATE TABLE %s (", tableName);
+
+    cursor = psListIteratorAlloc(table->list, 0);
+
+    // find column name and type
+    while ((item = psListGetNext(cursor))) {
+        if (item->type == PS_META_PRIMITIVE) {
+            // + column name + _ + column type
+            colType = psDBPTypeToSQL(item->pType);
+            psStringAppend(&query, "%s %s", item->name, colType);
+            psFree(colType);
+        } else if ((item->type == PS_META_STR) && (item->pType == PS_TYPE_PTR)) {
+            // + column name + _ + varchar( + length + )
+            psStringAppend(&query, "%s VARCHAR(%s)", item->name, item->data.V);
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                "Only types of PS_META_PRIMITIVE and PS_META_STR are supported.");
+
+            psFree(query);
+            psFree(cursor);
+
+            return NULL;
+        }
+
+        // add a , after every column declaration except the last one
+        if (!cursor->offEnd) {
+            psStringAppend(&query, ", ");
+        }
+    }
+
+    psListIteratorSet(cursor, 0);
+
+    // find database indexes
+    while ((item = psListGetNext(cursor))) {
+        if ((strncmp(item->comment, "Primary Key", strlen("Primary Key"))) == 0) {
+            psStringAppend(&query, ", PRIMARY KEY(%s)", item->name);
+        } else if ((strncmp(item->comment, "Key", strlen("Key"))) == 0) {
+            psStringAppend(&query, ", KEY(%s)", item->name);
+        }
+    }
+
+    psFree(cursor);
+
+    // end column types + table type
+    psStringAppend(&query, ") ENGINE=innodb");
+
+    return query;
+}
+
+static char *psDBGenerateSelectRowSQL(const char *tableName, const char *col, psMetadata *where, const psU64 limit)
+{
+    char            *query = NULL;
+    char            *whereSQL;
+    char            *limitString;
+
+    // select all columns if col is NULL
+    if (col) {
+        psStringAppend(&query, "SELECT %s FROM %s", col, tableName);
+    } else {
+        psStringAppend(&query, "SELECT * FROM %s", tableName);
+    }
+
+    // select all rows if where is NULL
+    if (where) {
+        whereSQL = psDBGenerateWhereSQL(where);
+        if (!whereSQL) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "SQL substring generation failed.");
+
+            psFree(query);
+
+            return NULL;
+        }
+        psStringAppend(&query, " %s", whereSQL);
+        psFree(whereSQL);
+    }
+
+    // treat limit == 0 as "no limit"
+    if (limit) {
+        limitString = psDBIntToString(limit);
+        psStringAppend(&query, " LIMIT %s", limitString);
+        psFree(limitString);
+    }
+
+    return query;
+}
+
+static char *psDBGenerateInsertRowSQL(const char *tableName, psMetadata *row)
+{
+    char            *query = NULL;
+    psListIterator  *cursor;
+    psMetadataItem  *item;
+
+    psStringAppend(&query, "INSERT INTO %s (", tableName);
+
+    cursor = psListIteratorAlloc(row->list, 0);
+
+    // get field names
+    while ((item = psListGetNext(cursor))) {
+        psStringAppend(&query, item->name);
+
+        // + , + _ between every field name
+        if (!cursor->offEnd) {
+            psStringAppend(&query, ", ");
+        }
+    }
+
+    // end of field names
+    psStringAppend(&query, ") VALUES (");
+
+    psListIteratorSet(cursor, 0);
+
+    // create value place holders
+    while ((item = psListGetNext(cursor))) {
+        psStringAppend(&query, "?");
+
+        // + ", " between every place holder
+        if (!cursor->offEnd) {
+            psStringAppend(&query, ", ");
+        }
+    }
+
+    psFree(cursor);
+
+    // end of values
+    psStringAppend(&query, ")");
+
+    return query;
+}
+
+static char *psDBGenerateUpdateRowSQL(const char *tableName, psMetadata *where, psMetadata *values)
+{
+    char            *query = NULL;
+    char            *setSQL;
+    char            *whereSQL;
+
+    if ((!values) || (!where)) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, "values and where params may not be NULL.");
+
+        return NULL;
+    }
+
+    setSQL = psDBGenerateSetSQL(values);
+    if (!setSQL) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "SQL substring generation failed.");
+
+        return NULL;
+    }
+
+    whereSQL = psDBGenerateWhereSQL(where);
+    if (!whereSQL) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "SQL substring generation failed.");
+
+        return NULL;
+    }
+
+    psStringAppend(&query, "UPDATE %s %s %s", tableName, setSQL, whereSQL);
+    psFree(setSQL);
+    psFree(whereSQL);
+
+    return query;
+}
+
+static char *psDBGenerateDeleteRowSQL(const char *tableName, psMetadata *where)
+{
+    char            *query = NULL;
+    char            *whereSQL;
+
+    // delete all rows if where is NULL
+    if (!where) {
+        psStringAppend(&query, "TRUNCATE TABLE %s", tableName);
+
+        return query;
+    }
+    
+    whereSQL = psDBGenerateWhereSQL(where);
+    if (!whereSQL) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "SQL substring generation failed.");
+
+        return NULL;
+    }
+
+    psStringAppend(&query, "DELETE FROM %s %s", tableName, whereSQL);
+
+    return query;
+}
+
+static char *psDBGenerateWhereSQL(psMetadata *where)
+{
+    char            *query = NULL;
+    psListIterator  *cursor;
+    psMetadataItem  *item;
+
+    if (!where) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, "where param may not be NULL.");
+
+        return NULL;
+    }
+
+    query = psStringCopy("WHERE ");
+
+    cursor = psListIteratorAlloc(where->list, 0);
+
+    // find column name and match pattern
+    while ((item = psListGetNext(cursor))) {
+        // item->data must be a string
+        if (item->type == PS_META_STR) {
+            // + column name + _ + like + _ + ' + value + '
+            if (*(char *)item->data.V == '\0') {
+                psStringAppend(&query, "%s IS NULL", item->name);
+            } else {
+                psStringAppend(&query, "%s LIKE '%s'", item->name, item->data.V);
+            }
+        } else {
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Only a type of PS_META_STR is supported.");
+
+            psFree(cursor);
+            psFree(query);
+
+            return NULL;
+        }
+
+        // + " and " after every column declaration except the last one
+        if (!cursor->offEnd) {
+            psStringAppend(&query, " AND ");
+        }
+    }
+
+    psFree(cursor);
+
+    return query;
+}
+
+static char *psDBGenerateSetSQL(psMetadata *set)
+{
+    char            *query = NULL;
+    psListIterator  *cursor;
+    psMetadataItem  *item;
+
+    if (!set) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, "set param may not be NULL.");
+
+        return NULL;
+    }
+
+    query = psStringCopy("SET ");
+
+    cursor = psListIteratorAlloc(set->list, 0);
+
+    // find column name 
+    while ((item = psListGetNext(cursor))) {
+        // + column name + _ + = + _ + ?
+        psStringAppend(&query, "%s = ?", item->name);
+
+        // + ", " after every column declaration except the last one
+        if (!cursor->offEnd) {
+            psStringAppend(&query, ",  ");
+        }
+    }
+
+    psFree(cursor);
+
+    return query;
+}
+
+
+// lookup table functions
+/*****************************************************************************/
+
+static psElemType psDBMySQLToPType(enum enum_field_types type, unsigned int flags)
+{
+    psHash          *mysqlToSQLTable;   // type lookup table
+    psHash          *sqlToPSTable;      // type lookup table
+    char            *key;               // hash tmp value
+    char            *value;             // hash tmp value
+    char            *sqlType;           // copy of lookup table result
+    psU32           pType;              // psElemType of a field
+
+    mysqlToSQLTable = psDBGetMySQLToSQLTable();
+    sqlToPSTable    = psDBGetSQLToPTypeTable();
+
+    // lookup MySQL column type
+    key     = psDBIntToString((psU64)type); 
+    sqlType = psHashLookup(mysqlToSQLTable, key);
+    psFree(key);
+    psFree(mysqlToSQLTable);
+
+    if (!sqlType) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "type lookup failed.");
+
+        psFree(sqlToPSTable);
+
+        return -1;
+    }
+
+    // MySQL column types can not be directly translated to PS
+    // types as the the mysql types do not tell you if the value is
+    // signed or unsigned.  The result is this ugly conversion from
+    // mysql -> ascii -> ptype
+    if (flags & UNSIGNED_FLAG) {
+        psStringPrepend(&sqlType, "UNSIGNED ");
+    }
+
+    // convert MySQL type to PS type
+    value = psHashLookup(sqlToPSTable, sqlType);
+    psFree(sqlToPSTable);
+
+    if (!value) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "type lookup failed.");
+
+        return -1;
+    }
+
+    pType = (psU32)atol(value);
+
+    return pType;
+}
+
+static char *psDBPTypeToSQL(psElemType pType)
+{
+    psHash          *pTypeToSQLTable;   // type lookup table
+    char            *key;               // hash tmp value
+    char            *sqlType;             // hash tmp value
+
+    pTypeToSQLTable = psDBGetPTypeToSQLTable();
+
+    key = psDBIntToString((psU64)pType);
+    sqlType = psHashLookup(pTypeToSQLTable, key);
+    psFree(key);
+    psFree(pTypeToSQLTable);
+
+    if (!sqlType) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "type lookup failed.");
+
+        return NULL;
+    }
+
+    psMemIncrRefCounter(sqlType);
+
+    return sqlType;
+}
+
+static mysqlType *psDBPTypeToMySQL(psElemType pType)
+{
+    psHash          *pTypeToMySQLTable; // type lookup table
+    char            *key;               // hash tmp value
+    mysqlType       *mType;             // mysqlType struct to return
+    
+    pTypeToMySQLTable = psDBGetPTypeToMySQLTable();
+
+    key = psDBIntToString((psU64)pType);
+    mType = psHashLookup(pTypeToMySQLTable, key);
+    psFree(key);
+    psFree(pTypeToMySQLTable);
+
+    if (!mType) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "type lookup failed.");
+
+        return NULL;
+    }
+
+    psMemIncrRefCounter(mType);
+
+    return mType;
+}
+
+static psHash *psDBGetPTypeToSQLTable(void)
+{
+    static psHash   *lookupTable = NULL;
+
+    if (!lookupTable) {
+        lookupTable = psHashAlloc(14);
+
+        // no support for CHAR, TEXT or GLOB
+        psDBAddToLookupTable(lookupTable, PS_TYPE_S8,  "TINYINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_S16, "SMALLINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_S32, "INT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_S64, "BIGINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_U8,  "UNSIGNED TINYINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_U16, "UNSIGNED SMALLINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_U32, "UNSIGNED INT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_U64, "UNSIGNED BIGINT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_F32, "FLOAT");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_F64, "DOUBLE");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_C32, "PS_TYPE_C32 is not supported");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_C64, "PS_TYPE_C64 is not supported");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_BOOL,"BOOLEAN");
+        psDBAddToLookupTable(lookupTable, PS_TYPE_PTR, "VARCHAR");
+    }
+
+    // simulate true ref counting
+    psMemIncrRefCounter(lookupTable);
+
+    return lookupTable;
+}
+
+static void psDBPTypeToSQLTableCleanup(void) 
+{
+    psHash          *lookupTable;
+
+    lookupTable = psDBGetPTypeToSQLTable();
+
+    psMemDecrRefCounter(lookupTable);
+    psFree(lookupTable);
+}
+
+static psHash *psDBGetSQLToPTypeTable(void)
+{
+    static psHash   *lookupTable = NULL;
+    psHash          *psToSQLTable;
+    psList          *list;
+    psListIterator  *cursor;
+    char            *key;
+    char            *value;
+
+    if (!lookupTable) {
+        // invert the PSToSQL table
+        psToSQLTable = psDBGetPTypeToSQLTable();
+        lookupTable = psHashAlloc(psToSQLTable->nbucket);
+
+        list = psHashKeyList(psToSQLTable);
+        cursor = psListIteratorAlloc(list, 0); 
+
+        while ((key = psListGetNext(cursor))) {
+            value = psHashLookup(psToSQLTable, key);
+            // switch key and value
+            psHashAdd(lookupTable, value, key);
+        }
+
+        // DECIMAL does not exist in the pType to SQL table
+        value = psDBIntToString((psU64)PS_TYPE_PTR);
+        psHashAdd(lookupTable, "DECIMAL", value);
+        psFree(value);
+
+        psFree(cursor);
+        psFree(list);
+        psFree(psToSQLTable);
+    }
+
+    // simulate true ref counting
+    psMemIncrRefCounter(lookupTable);
+
+    return lookupTable;
+}
+
+static void psDBSQLToPTypeTableCleanup(void) 
+{
+    psHash          *lookupTable;
+
+    lookupTable = psDBGetSQLToPTypeTable();
+
+    psMemDecrRefCounter(lookupTable);
+    psFree(lookupTable);
+}
+
+static psHash *psDBGetMySQLToSQLTable(void)
+{
+    static psHash   *lookupTable = NULL;
+
+    if (!lookupTable) {
+        lookupTable = psHashAlloc(20);
+
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_TINY,      "TINYINT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_SHORT,     "SMALLINT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_LONG,      "INT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_INT24,     "MEDIUMINT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_LONGLONG,  "BIGINT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_DECIMAL,   "DECIMAL");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_FLOAT,     "FLOAT");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_DOUBLE,    "DOUBLE");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_TIMESTAMP, "TIMESTAMP");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_DATE,      "DATE");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_TIME,      "TIME");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_DATETIME,  "DATETIME");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_YEAR,      "YEAR");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_STRING,    "CHAR");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_VAR_STRING,"VARCHAR");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_BLOB,      "BLOB");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_SET,       "SET");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_ENUM,      "ENUM");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_NULL,      "NULL-type");
+        psDBAddToLookupTable(lookupTable, FIELD_TYPE_CHAR,      "TINYINT");
+    }
+
+    // simulate true ref counting
+    psMemIncrRefCounter(lookupTable);
+
+    return lookupTable;
+}
+
+static void psDBMySQLToSQLTableCleanup(void)
+{
+    psHash          *lookupTable;
+
+    lookupTable = psDBGetMySQLToSQLTable();
+
+    psMemDecrRefCounter(lookupTable);
+    psFree(lookupTable);
+}
+
+static psHash *psDBGetPTypeToMySQLTable(void)
+{
+    static psHash   *lookupTable = NULL;
+
+    if (!lookupTable) {
+        lookupTable = psHashAlloc(14);
+
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_S8,     psDBMySQLTypeAlloc(MYSQL_TYPE_TINY,       false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_S16,    psDBMySQLTypeAlloc(MYSQL_TYPE_SHORT,      false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_S32,    psDBMySQLTypeAlloc(MYSQL_TYPE_LONG,       false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_S64,    psDBMySQLTypeAlloc(MYSQL_TYPE_LONGLONG,   false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_U8,     psDBMySQLTypeAlloc(MYSQL_TYPE_TINY,       true));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_U16,    psDBMySQLTypeAlloc(MYSQL_TYPE_SHORT,      true));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_U32,    psDBMySQLTypeAlloc(MYSQL_TYPE_LONG,       true));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_U64,    psDBMySQLTypeAlloc(MYSQL_TYPE_LONGLONG,   true));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_F32,    psDBMySQLTypeAlloc(MYSQL_TYPE_FLOAT,      false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_F64,    psDBMySQLTypeAlloc(MYSQL_TYPE_DOUBLE,     false));
+        // bogus type
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_C32,    psDBMySQLTypeAlloc(MYSQL_TYPE_VAR_STRING, false));
+        // bogus type
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_C64,    psDBMySQLTypeAlloc(MYSQL_TYPE_VAR_STRING, false));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_BOOL,   psDBMySQLTypeAlloc(MYSQL_TYPE_TINY,       true));
+        psDBAddVoidToLookupTable(lookupTable, PS_TYPE_PTR,    psDBMySQLTypeAlloc(MYSQL_TYPE_VAR_STRING, false));
+    }
+
+    // simulate true ref counting
+    psMemIncrRefCounter(lookupTable);
+
+    return lookupTable;
+}
+
+static void psDBPTypeToMySQLTableCleanup(void)
+{
+    psHash          *lookupTable;
+
+    lookupTable = psDBGetPTypeToMySQLTable();
+
+    psMemDecrRefCounter(lookupTable);
+    psFree(lookupTable);
+}
+
+static psPtr psDBMySQLTypeAlloc(enum enum_field_types type, bool isUnsigned)
+{
+    mysqlType       *mType;
+
+    mType = psAlloc(sizeof(mysqlType));
+    mType->type       = type;
+    mType->isUnsigned = isUnsigned;
+
+    return mType;
+}
+
+static void psDBAddToLookupTable(psHash *lookupTable, psU32 type, const char *string)
+{
+    char            *key;
+    char            *value;
+
+    key = psDBIntToString((psU64)type);
+    value = psStringCopy(string);
+
+    psHashAdd(lookupTable, key, value);
+
+    psFree(key);
+    psFree(value);
+}
+
+static void psDBAddVoidToLookupTable(psHash *lookupTable, psU32 type, psPtr value)
+{
+    char            *key;
+
+    key = psDBIntToString((psU64)type);
+
+    psHashAdd(lookupTable, key, value);
+
+    // destructive of value parameter
+    psFree(value);
+    psFree(key);
+}
+
+
+// pType utility functions
+/*****************************************************************************/
+
+#define PS_NAN_ALLOC(dest, type, nan) \
+    dest = psAlloc(sizeof(type)); \
+    *(type *)dest = nan;
+
+static psPtr psDBGetPTypeNaN(psElemType pType)
+{
+    psPtr           myNaN;
+
+    switch (pType) {
+        case PS_TYPE_S8:
+            PS_NAN_ALLOC(myNaN, psS8, PS_MAX_S8);
+            break;
+        case PS_TYPE_S16:
+            PS_NAN_ALLOC(myNaN, psS16, PS_MAX_S16);
+            break;
+        case PS_TYPE_S32:
+            PS_NAN_ALLOC(myNaN, psS32, PS_MAX_S32);
+            break;
+        case PS_TYPE_S64:
+            PS_NAN_ALLOC(myNaN, psS64, PS_MAX_S64);
+            break;
+        case PS_TYPE_U8:
+            PS_NAN_ALLOC(myNaN, psU8, PS_MAX_U8);
+            break;
+        case PS_TYPE_U16:
+            PS_NAN_ALLOC(myNaN, psU16, PS_MAX_U16);
+            break;
+        case PS_TYPE_U32:
+            PS_NAN_ALLOC(myNaN, psU32, PS_MAX_U32);
+            break;
+        case PS_TYPE_U64:
+            PS_NAN_ALLOC(myNaN, psU64, PS_MAX_U64);
+            break;
+        case PS_TYPE_F32:
+            PS_NAN_ALLOC(myNaN, psF32, NAN);
+            break;
+        case PS_TYPE_F64:
+            PS_NAN_ALLOC(myNaN, psF64, NAN);
+            break;
+        case PS_TYPE_C32:
+            // this is a bogus SQL type
+            PS_NAN_ALLOC(myNaN, psC32, NAN);
+            break;
+        case PS_TYPE_C64:
+            // this is a bogus SQL type
+            PS_NAN_ALLOC(myNaN, psC64, NAN);
+            break;
+        case PS_TYPE_BOOL:
+            // what is NaN for a bool?
+            break;
+        case PS_TYPE_PTR:
+            PS_NAN_ALLOC(myNaN, char, '\0');
+            break;
+    }
+
+    return myNaN;
+}
+
+#define PS_IS_NAN(type, data, nan) *(type *)data == nan
+
+static bool psDBIsPTypeNaN(psElemType pType, psPtr data)
+{
+    bool    isNaN;
+
+    switch (pType) {
+        case PS_TYPE_S8:
+            isNaN = PS_IS_NAN(psS8, data, PS_MAX_S8);
+            break;
+        case PS_TYPE_S16:
+            isNaN = PS_IS_NAN(psS16, data, PS_MAX_S16);
+            break;
+        case PS_TYPE_S32:
+            isNaN = PS_IS_NAN(psS32, data, PS_MAX_S32);
+            break;
+        case PS_TYPE_S64:
+            isNaN = PS_IS_NAN(psS64, data, PS_MAX_S64);
+            break;
+        case PS_TYPE_U8:
+            isNaN = PS_IS_NAN(psU8, data, PS_MAX_U8);
+            break;
+        case PS_TYPE_U16:
+            isNaN = PS_IS_NAN(psU16, data, PS_MAX_U16);
+            break;
+        case PS_TYPE_U32:
+            isNaN = PS_IS_NAN(psU32, data, PS_MAX_U32);
+            break;
+        case PS_TYPE_U64:
+            isNaN = PS_IS_NAN(psU64, data, PS_MAX_U64);
+            break;
+        case PS_TYPE_F32:
+            isNaN = PS_IS_NAN(psF32, data, NAN);
+            break;
+        case PS_TYPE_F64:
+            isNaN = PS_IS_NAN(psF64, data, NAN);
+            break;
+        case PS_TYPE_C32:
+            // this is a bogus SQL type
+            isNaN = PS_IS_NAN(psC32, data, NAN);
+            break;
+        case PS_TYPE_C64:
+            // this is a bogus SQL type
+            isNaN = PS_IS_NAN(psC64, data, NAN);
+            break;
+        case PS_TYPE_BOOL:
+            // what is NaN for a bool?
+            break;
+        case PS_TYPE_PTR:
+            isNaN = PS_IS_NAN(char, data, '\0');
+            break;
+    }
+
+    return isNaN;
+}
+
+
+// string utility functions
+/*****************************************************************************/
+
+static char *psDBIntToString(psU64 n)
+{
+    char            *string;
+    size_t          length;
+
+    // length of string + \0
+    // if n is 0, length is 1 char + \0
+    length = n ? (size_t)log10((double)n) + 1
+               : 2;
+    string = psAlloc(length);
+    sprintf(string, "%li", n);
+
+    return string;
+}
+
+static ssize_t psStringAppend(char **dest, const char *format, ...)
+{
+    va_list         args;
+    size_t          length;             // complete string length (sans \0)
+    size_t          oldLength;          // original string length (sans \0)
+    ssize_t         tailLength;         // length of string to append
+
+    if (!*dest) {
+        *dest = psStringCopy("");
+        oldLength = 0;
+    } else {
+        // size of existing string
+        oldLength = strlen(*dest);
+    }
+
+    // find the size of the string to append
+    va_start(args, format);
+    // C99 guarentees vsnprintf() to work as expected with size = 0
+    tailLength = vsnprintf(*dest, 0, format, args);
+    va_end(args);
+
+    // if the new tail is zero length, return the length of the old string.  if
+    // it's a format error, return the error code.
+    if (tailLength < 1) {
+        return tailLength == 0 ? oldLength : tailLength;
+    }
+
+    // new string length (sans \0)
+    length = oldLength + tailLength;
+
+    // realloc string to string + tail + \0
+    *dest = psRealloc(*dest, length + 1);
+
+    // append tail + \0
+    va_start(args, format);
+    vsnprintf(*dest + oldLength, tailLength + 1, format, args);
+    va_end(args);
+
+    return length;
+}
+
+static ssize_t psStringPrepend(char **dest, const char *format, ...)
+{
+    va_list         args;
+    size_t          length;             // complete string length (sans \0)
+    ssize_t         headLength;         // length of string to prepend
+    char            *oldDest;           // copy of original string
+
+    if (!*dest) {
+        // makes the string backup and concatination pointless 
+        *dest = psStringCopy("");
+        length = 0;
+    } else {
+        // size of existing string
+        length = strlen(*dest);
+    }
+
+    // find the size of the string to prepend
+    va_start(args, format);
+    // C99 guarentees vsnprintf() to work as expected with size = 0
+    headLength = vsnprintf(*dest, 0, format, args);
+    va_end(args);
+
+    // if the new head is zero length, return the length of the old string.  if
+    // it's a format error, return the error code.
+    if (headLength < 1) {
+        return headLength == 0 ? length : headLength;
+    }
+
+    // backup original string
+    oldDest = psStringCopy(*dest);
+
+    // new string length (sans \0)
+    length += headLength;
+
+    // realloc string to head + string + \0
+    *dest = psRealloc(*dest, length + 1);
+
+    // copy the new head to the beginning of string
+    va_start(args, format);
+    vsnprintf(*dest, length + 1, format, args);
+    va_end(args);
+
+    // append the original string
+    strncat(*dest, oldDest, length + 1);
+
+    psFree(oldDest);
+
+    return length;
+}
Index: /tags/rel-0_0_1/archive/psdb/psDB.h
===================================================================
--- /tags/rel-0_0_1/archive/psdb/psDB.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/psDB.h	(revision 7462)
@@ -0,0 +1,261 @@
+/** @file  psDB.h
+ *
+ *  @brief database types and functions
+ *
+ *  This file defines the abstract database type and functions that
+ *  perform basic database operations.
+ *
+ *  @ingroup DataBase
+ *
+ *  @author Joshua Hoblitt
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-02-16 00:33:32 $
+ *
+ *  Copyright 2005 Joshua Hoblitt, University of Hawaii
+ */
+
+#ifndef PS_DB_H
+#define PS_DB_H 1
+
+#include <mysql/mysql.h>
+#include <pslib.h>
+
+/// @addtogroup DataBase
+/// @{
+
+/** Database handle
+ *
+ *  An opaque object representing a database connection.
+ *
+ */
+typedef struct {
+    MYSQL *mysql;   ///< MySQL database handle
+} psDB;
+
+/** Opens a new database connection
+ *
+ *  @return A new psDB object if the database connection is successful or NULL on
+ *  failure.
+ */
+psDB *psDBInit(
+    const char *host,                   ///< Database server hostname
+    const char *user,                   ///< Database username
+    const char *passwd,                 ///< Database password
+    const char *dbname                  ///< Database namespace
+);
+
+/** Closes a database connection
+ */
+void psDBCleanup(
+    psDB *dbh                           ///< Database handle
+);
+
+/** Creates a new database namespace
+ *
+ * @return true on success
+ */
+bool psDBCreate(
+    psDB *dbh,                          ///< Database handle
+    const char *dbname                  ///< New database namespace
+);
+
+/** Changes the current database namespace
+ *
+ * @return true on success
+ */
+bool psDBChange(
+    psDB *dbh,                          ///< Database handle
+    const char *dbname                  ///< Database namespace
+);
+
+/** Drops a database namespace
+ *
+ * @return true on success
+ */
+bool psDBDrop(
+    psDB *dbh,                          ///< Database handle
+    const char *dbname                  ///< Database namespace
+);
+
+/** Creates a new database table
+ *
+ * This function generates and executes the SQL needed to create a table named
+ * "tableName", with the column names and data types as described in "md".  Each
+ * data item in the psMetadata collection represents a single table field.  The
+ * name of the field is given by the name of the psMetadataItem and the data
+ * type is give by the psMetadataItem.type and psMetadataItem.ptype entries.  A
+ * lookup table should be used to convert from PSLib types into MySQL
+ * compatible SQL data types.  For example, a PS_META_STR would map to an SQL99
+ * varchar.  If the value of type is PS_META_STR then the psMetadataItem.data
+ * element is set to a string with the length for the field written as a text
+ * string.  The value of the psMetadataItem.data element is unused for the
+ * PS_META_PRIMITIVE types.  Other psMetadata types beyond PS_META_STR and
+ * PS_META_PRIMITIVE are not allowed in a table definition.  
+ *
+ * Database indexes can be specified setting the "comment" field to "Primary
+ * Key" or "Key".  Comments are otherwise ignored.
+ *
+ * @return true on success
+ */
+bool psDBCreateTable(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    psMetadata *md                      ///< Column names, types, and indexes
+);
+
+/** Deletes a database table
+ *
+ * @return true on success
+*/
+bool psDBDropTable(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName               ///< Table name
+);
+
+/** Selects a column from a table
+ *
+ * This function generates and executes the SQL needed to select an entire
+ * column from a table or up to "limit" rows from it.  If "limit" is 0, the
+ * entire range is returned.
+ *
+ * @return A psArray of strings or NULL on failure
+ */
+psArray *psDBSelectColumn(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    const char *col,                    ///< Column name
+    const psU64 limit                   ///< Maximum number of elements to return
+);
+
+/** Selects a column from a table and casts it to a given type
+ *
+ * This function generates and executes the SQL needed to select an entire
+ * column from a table or up to "limit" rows from it.  If "limit" is 0, the
+ * entire range is returned.  The data in the column is cast to to "pType".
+ *
+ * @return A psVector or NULL on failure
+ */
+psVector *psDBSelectColumnNum(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    const char *col,                    ///< Column name
+    psElemType pType,                   ///< Resulting psVector type
+    const psU64 limit                   ///< Maximum number of elements to return
+);
+
+/** Selects a set of rows from a table
+ *
+ * This function returns rows from the specified table which match the
+ * restrictions given by "where".  The restrictions are specified as field /
+ * value pairs.  The psMetadata collection "where" must consist of valid
+ * database fields.  The selected rows are returned as a psArray of psMetadata
+ * values, one per row.
+ *
+ * Currently, the "where" specification only supports the PS_META_STR type.
+ * The string value can be a SQL match pattern, e.g. "%foo%", or an empty
+ * string, e.g. "", to match NULL field values.
+ *
+ * @return A psArray of psMetadata or NULL on failure
+ */
+psArray *psDBSelectRows(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    psMetadata *where,                  ///< Row match criteria
+    const psU64 limit                   ///< Maximum number of elements to return
+);
+
+/** Insert a single row into a table
+ *
+ * This function inserts the data from "row" into "tableName".
+ *
+ * The "row" specification uses the psMetadataItem name as the column name.
+ * The field values maybe specified in any order.  psMetadata types beyond
+ * PS_META_STR and PS_META_PRIMITIVE are not supported.  If fields are
+ * specified in "row" that do not exist in "tableName", the insert will fail.
+ *
+ * @return true on success
+ */
+bool psDBInsertOneRow(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    psMetadata *row                     ///< Row description
+);
+
+/** Insert a set of rows into a table
+ *
+ * This function inserts the data from "rowSet" into "tableName".
+ *
+ * "rowSet" is a psArray of psMetadata containing row specifications identical to
+ * those used in psDBInsertOneRow().
+ *
+ * @return true on success
+ */
+bool psDBInsertRows(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    psArray *rowSet                     ///< Set of rows to insert
+);
+
+/** Retrieves all rows from a table
+ *
+ * This function fetches all rows as an psArray of psMetadata.  The rows are in
+ * the same psMetadata format as used in psDBInsertOneRow() & psDBInsertRows().
+ *
+ * @return A psArray of psMetadata or NULL on failure
+ */
+psArray *psDBDumpRows(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName               ///< Table name
+);
+
+/** Retrieves all columns from a table
+ *
+ * This function fetches all columns, as either a psVector or a psArray
+ * depending on whether or not the column is numeric, and return them in a
+ * psMetadata structure where psMetadataItem.name contains the column's name.
+ *
+ * @return A psMetadata containing either a psArrays or psVector per column
+ */
+psMetadata *psDBDumpCols(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName               ///< Table name
+);
+
+/** Updates the field values, as specified, in a table
+ *
+ * This function updates the fields contained in "values" in the row(s) that
+ * have a field with the value indicated by "where".  Where "where" is in the
+ * same format as used in psDBSelectRows().
+ *
+ * The "values" specification uses the same format as the row specification
+ * used in psDBInsertOneRow(), etc.
+ *
+ * @return The number of rows modified or a negative value on error
+ */
+psS64 psDBUpdateRows(
+    psDB *dbh,                          ///< Database handle
+    const char *tableName,              ///< Table name
+    psMetadata *where,                  ///< Row match criteria
+    psMetadata *values                  ///< new field values
+);
+
+/** Deletes rows, as specified, in a table
+ *
+ * Delete the rows that are matched by "where" using the same semantics for
+ * "where" as in psDBUpdateRow().
+ *
+ * If "where" is NULL, all rows in the table will be removed and regardless of
+ * the number of rows that were dropped, only 1 will be returned on success.
+ *
+ * @return The number of rows removed or a negative value on error
+ */
+psS64 psDBDeleteRows(
+    psDB *dbh,                          ///< Database handle
+     const char *tableName,             ///< Table name
+     psMetadata *where                  ///< Row match criteria
+);
+
+/// @}
+
+#endif // PS_DB_H
Index: /tags/rel-0_0_1/archive/psdb/psleak.c
===================================================================
--- /tags/rel-0_0_1/archive/psdb/psleak.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/psleak.c	(revision 7462)
@@ -0,0 +1,61 @@
+#include <pslib.h>
+#include "psleak.h"
+
+#define LEAKS "leaks.dat"       // File to which to write leaks data
+
+
+psMemoryId stacMemPrint(const psMemBlock *ptr)
+{
+    psLogMsg("stac.memoryPrint", PS_LOG_INFO,
+         "Memory block %d:\n"
+         "\tFile %s, line %d, size %d\n"
+         "\tPosts: %x %x %x\n",
+         ptr->id, ptr->file, ptr->lineno, ptr->userMemorySize, ptr->startblock, ptr->endblock,
+         *(void**)((int8_t *)(ptr + 1) + ptr->userMemorySize));
+    return 0;
+}
+
+
+void stacMemoryProblem(const psMemBlock* ptr, ///< the pointer to the problematic memory block.
+               const char *file, ///< the file in which the problem originated
+               psS32 lineno ///< the line number in which the problem originated
+    )
+{
+    psLogMsg("stac.checkMemory.corruption", PS_LOG_WARN,
+         "Memory corruption detected in memBlock %d\n"
+         "\tFile %s, line %d, size %d\n"
+         "\tPosts: %x %x %x\n",
+         ptr->id, file, lineno, ptr->userMemorySize, ptr->startblock, ptr->endblock,
+         (ptr + 1 + ptr->userMemorySize));
+}
+
+
+
+void stacCheckMemory(void)
+{
+    psMemBlock **leaks = NULL;      // List of leaks
+    FILE *leakFile;         // File to write leaks to
+
+    psTrace("stac.checkMemory", 1, "Checking for memory problems....\n");
+
+    (void)psMemProblemCallbackSet(stacMemoryProblem); // Set callback for corruption
+
+    if ((leakFile = fopen(LEAKS, "w")) == NULL) {
+    //psError("stac.checkMemory", "Unable to open leaks file, %s\n",LEAKS);
+    return;
+    }
+
+    int nLeaks = psMemCheckLeaks(0, &leaks, leakFile, false); // Number of leaks
+    psTrace("stac.checkMemory", 1, "%d leaks found.\n", nLeaks);
+    for (int i = 0; i < nLeaks; i++) {
+    psLogMsg("stac.checkMemory.leaks", PS_LOG_WARN,
+         "Memory leak detection: memBlock %d\n"
+         "\tFile %s, line %d, size %d\n",
+         leaks[i]->id, leaks[i]->file, leaks[i]->lineno, leaks[i]->userMemorySize);
+    }
+
+    int nCorrupted = psMemCheckCorruption(false); // Number of corrupted
+    psTrace("stac.checkMemory", 1, "%d memory blocks corrupted.\n", nCorrupted);
+
+}
+
Index: /tags/rel-0_0_1/archive/psdb/psleak.h
===================================================================
--- /tags/rel-0_0_1/archive/psdb/psleak.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/psleak.h	(revision 7462)
@@ -0,0 +1,15 @@
+#include <pslib.h>
+
+// Check memory
+void stacCheckMemory(void);
+
+// Print out the problem
+void stacMemoryProblem(const psMemBlock* ptr, ///< the pointer to the problematic memory block.
+               const char *file, ///< the file in which the problem originated
+               psS32 lineno ///< the line number in which the problem originated
+    );
+
+// Print out a memblock when it's allocated --- this function used as a callback
+psMemoryId stacMemPrint(const psMemBlock *ptr);
+
+
Index: /tags/rel-0_0_1/archive/psdb/test.c
===================================================================
--- /tags/rel-0_0_1/archive/psdb/test.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/psdb/test.c	(revision 7462)
@@ -0,0 +1,322 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "psDB.h"
+#include "psleak.h"
+
+#define TEST_SETUP\
+    int testsRun    = 0;\
+    int testsPassed = 0;
+
+#define TEST_SUMMARY\
+    printf("\n\n### TEST SUMMARY ###\n\n    %d of %d tests succeeded\n\n", \
+        testsPassed, testsRun);
+
+#define OK(test, name) \
+    testsRun++; \
+    printf("Test #%d: %s... ", testsRun, name); \
+    if (test) { \
+        testsPassed++; \
+        printf("OK\n"); \
+    } else { \
+        printf("FAILED\n"); \
+    }
+
+int main (int argv, char **argc)
+{ 
+    TEST_SETUP;
+
+    psMemAllocateCallbackSet(stacMemPrint);
+    //psMemAllocateCallbackSetID(224);
+    psMemFreeCallbackSet(stacMemPrint);
+    psMemProblemCallbackSet(stacMemoryProblem);
+    atexit(stacCheckMemory);
+
+    psDB            *dbh;
+
+    // open db
+    dbh = psDBInit("localhost", "test", NULL, "test");
+    OK(dbh, "psDBInit() - connect");
+
+    {
+        psDB        *foo;
+
+        foo = psDBInit("tsohlacol", "tset", NULL, "tset");
+        OK(!foo, "psDBInit(), bad DSN");
+    }
+
+    // the next 3 tests will fail as the MySQL test account has insufficent
+    // permissions
+    OK(psDBCreate(dbh, "foobar"), "psDBCreate()");
+    OK(psDBChange(dbh, "foobar"), "psDBChange()");
+    OK(psDBDrop(dbh, "foobar"), "psDBDrop()");
+
+    // psDBCreateTable() & psDBDropTable()
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "foo", PS_TYPE_S32, PS_META_PRIMITIVE, "Primary Key"); 
+        psMetadataAdd(md, 1, "bar", PS_TYPE_BOOL,PS_META_PRIMITIVE, "Key"); 
+        psMetadataAdd(md, 2, "baz", PS_TYPE_F64, PS_META_PRIMITIVE, NULL);
+        psMetadataAdd(md, 3, "boing", PS_TYPE_PTR, PS_META_STR, NULL, "60");
+
+        OK(psDBCreateTable(dbh, "bar", md), "psDBCreateTable() - create table");
+
+        psFree(md);
+    }
+
+    OK(psDBDropTable(dbh, "bar"), "psDBDropTable() - drop table");
+
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "foo", PS_TYPE_S32, PS_META_PRIMITIVE, "Primary Key"); 
+
+        psDBCreateTable(dbh, "bar", md);
+
+        OK(!psDBCreateTable(dbh, "bar", md), "psDBCreateTable() - table already exists");
+
+        psFree(md);
+
+        psDBDropTable(dbh, "bar");
+    }
+
+    OK(!psDBDropTable(dbh, "fubar"), "psDBDropTable() - non-existant table");
+
+    // setup yak table for later tests
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "hair", PS_TYPE_F32, PS_META_PRIMITIVE, NULL);
+
+        psDBCreateTable(dbh, "yak", md);
+
+        psFree(md);
+    }
+
+    // setup horse table for later tests
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "100");
+
+        psDBCreateTable(dbh, "horse", md);
+
+        psFree(md);
+    }
+
+    // psDBInsertOneRow()
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "hair", PS_TYPE_F32, PS_META_PRIMITIVE, NULL, 10e3);
+
+        OK(psDBInsertOneRow(dbh, "yak", md),"psDBInsertOneRow() - number");
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "hair", PS_TYPE_F32, PS_META_PRIMITIVE, NULL, NAN);
+
+        OK(psDBInsertOneRow(dbh, "yak", md),"psDBInsertOneRow() - nan");
+
+        psFree(md);
+    }
+
+    // psDBInsertRows()
+    {
+        psArray         *rowSet;
+        psMetadata      *row;
+
+        rowSet = psArrayAlloc(3);
+        rowSet->n = 0;
+
+        row = psMetadataAlloc();
+        psMetadataAdd(row, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "brown"); 
+        psArrayAdd(rowSet, 0, row);
+        // FIXME
+        // psFree(row);
+
+        row = psMetadataAlloc();
+        psMetadataAdd(row, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, ""); 
+        psArrayAdd(rowSet, 0, row);
+        // FIXME
+        // psFree(row);
+
+        row = psMetadataAlloc();
+        psMetadataAdd(row, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "pink"); 
+        psArrayAdd(rowSet, 0, row);
+        // FIXME
+        // psFree(row);
+
+        OK(psDBInsertRows(dbh, "horse", rowSet), "psDBInsertRows() - multi-row insert");
+
+        psFree(rowSet);
+    }
+
+    // psDBSelectColumn()
+    {
+        psArray         *column;
+        int             i;
+
+        column = psDBSelectColumn(dbh, "horse", "color", 42);
+        OK(column, "psDBSelectColumn() - select");
+        OK(column->n == 3, "psDBSelectColumn() - number of elements");
+
+        for (i = 0; i < column->n; i++) {
+            printf("\thorse color is: %s\n", (char *)column->data[i]);
+        }
+
+        psFree(column);
+    }
+
+    // psDBSelectColumnNum()
+    {
+        psVector        *column;
+        int             i;
+
+        column = psDBSelectColumnNum(dbh, "yak", "hair", PS_TYPE_F32, 42);
+        OK(column, "psDBSelectColumnNum() - select");
+        OK(column->n == 2, "psDBSelectColumnNum() - number of elements");
+
+        for (i = 0; i < column->n; i++) {
+            printf("\tyak hair count is: %f\n", column->data.F32[i]);
+            if (isnan(column->data.F32[i])){
+                printf("\t\tNO yak hair!!! (it's a nan)\n");
+            }
+        }
+
+        psFree(column);
+    }
+
+    // psDBSelectRows()
+    {
+        psMetadata      *md;
+        psMetadataItem  *item;
+        psArray         *resultSet;
+        psListIterator  *cursor;
+        int             i;
+
+        md = psMetadataAlloc();
+        psMetadataAdd(md, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "brown"); 
+
+        resultSet = psDBSelectRows(dbh, "horse", md, 0);
+        OK(resultSet, "psDBSelectRows() - select");
+        OK(resultSet->n == 1, "psDBSelectRows() - number of rows");
+
+        psFree(md);
+        for (i = 0; i < resultSet->n; i++) {
+            md = (psMetadata *)resultSet->data[0];
+
+            cursor = psListIteratorAlloc(md->list, 0);
+
+            while ((item = psListGetNext(cursor))) {
+                printf("\tcolum name is: %s, value is: %s\n", item->name, (char *)item->data.V);
+            }
+
+            psFree(cursor);
+        }
+        psFree(resultSet);
+
+    }
+
+
+    // psDBDumpRows()
+    {
+        psArray         *resultSet;
+
+        resultSet = psDBDumpRows(dbh, "horse");
+        OK(resultSet, "psDBDumpRows() - dump");
+        OK(resultSet->n == 3, "psDBDumpRows() - number of rows");
+        psFree(resultSet);
+    }
+
+    // psDBDumpCols()
+    {
+        psMetadata      *columns;
+        psMetadataItem  *item;
+        psListIterator  *cursor;
+
+        columns = psDBDumpCols(dbh, "horse");
+        OK(columns, "psDBDumpCols() - dump");
+        OK(columns->list->size == 1, "psDBDumpCols() - number of columns");
+
+        cursor = psListIteratorAlloc(columns->list, 0);
+
+        while ((item = psListGetNext(cursor))) {
+            printf("\tcolumn name is: %s\n", item->name);
+        }
+
+        psFree(cursor);
+        psFree(columns);
+    }
+
+    // psDBUpdateRows()
+    {
+        psMetadata      *where;
+        psMetadata      *value;
+        psU64           rowsAffected;
+        psArray         *resultSet;
+        int             i;
+        psListIterator  *cursor;
+        psMetadata      *md;
+        psMetadataItem  *item;
+
+        where = psMetadataAlloc();
+        psMetadataAdd(where, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "pink"); 
+
+        value = psMetadataAlloc();
+        psMetadataAdd(value, 0, "color", PS_TYPE_PTR, PS_META_STR, NULL, "HOT pink"); 
+
+        rowsAffected = psDBUpdateRows(dbh, "horse", where, value);
+        psFree(where);
+        psFree(value);
+
+        OK(rowsAffected, "psDBUpdateRows() - pink -> HOT pink");
+
+        resultSet = psDBSelectRows(dbh, "horse", value, 0);
+
+        for (i = 0; i < resultSet->n; i++) {
+            md = (psMetadata *)resultSet->data[0];
+
+            cursor = psListIteratorAlloc(md->list, 0);
+
+            while ((item = psListGetNext(cursor))) {
+                if (strcmp((char *)item->data.V, "HOT pink") == 0) {
+                    OK(1, "psDBUpdateRows() - psDBSelectRows() found HOT pink");
+                    printf("\thorse color is: %s\n", (char *)item->data.V);
+                }
+            }
+
+            psFree(cursor);
+        }
+
+        psFree(resultSet);
+    }
+
+    // psDBDeleteRows()
+    OK(psDBDeleteRows(dbh, "yak", NULL), "psDBDeleteRows()");
+    
+    // cleanup other tests
+    psDBDropTable(dbh, "horse");
+    psDBDropTable(dbh, "yak");
+
+    // close db
+    psDBCleanup(dbh);
+
+    TEST_SUMMARY;
+
+    //psErrorStackPrint(stderr, "### Error stack dump ###\n");
+
+    exit(0);
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/.cvsignore
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/.cvsignore	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/.cvsignore	(revision 7462)
@@ -0,0 +1,16 @@
+Makefile.in
+aclocal.m4
+autom4te.cache
+configure
+Makefile
+config.log
+config.status
+compile
+config.guess
+config.sub
+depcomp
+install-sh
+ltmain.sh
+missing
+stamp-h1
+.deps
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/Makefile.am
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/Makefile.am	(revision 7462)
@@ -0,0 +1,52 @@
+bin_PROGRAMS = phase2
+
+phase2_CPPFLAGS = $(PSLIB_CFLAGS) $(phase2_CFLAGS)
+phase2_LDADD = $(PSLIB_LIBS)
+phase2_SOURCES = \
+	papPhase2.c \
+	papStuff.c \
+	pmChipMosaic.c \
+	pmConfig.c \
+	pmFPA.c \
+	pmFPAConceptsGet.c \
+	pmFPAConceptsSet.c \
+	pmFPAConstruct.c \
+	pmFPARead.c \
+	pmFPAWrite.c \
+	pmFlatField.c \
+	pmMaskBadPixels.c \
+	pmNonLinear.c \
+	pmSubtractBias.c \
+	psAdditionals.c
+
+noinst_HEADERS = \
+	papStuff.h \
+	pmChipMosaic.h \
+	pmConfig.h \
+	pmFPA.h \
+	pmFPAConceptsGet.h \
+	pmFPAConceptsSet.h \
+	pmFPAConstruct.h \
+	pmFPARead.h \
+	pmFPAWrite.h \
+	pmFlatField.h \
+	pmFlatFieldErrors.h \
+	pmMaskBadPixels.h \
+	pmMaskBadPixelsErrors.h \
+	pmNonLinear.h \
+	pmSubtractBias.h \
+	psAdditionals.h
+
+EXTRA_DIST = \
+	autogen.sh \
+	gpc1_raw.config \
+	ipprc.config \
+	megacam_raw.config \
+	phase2.config \
+	nonlin.dat
+
+clean-local:
+	-rm -f TAGS
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/autogen.sh
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/autogen.sh	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/autogen.sh	(revision 7462)
@@ -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=phase2
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL=aclocal
+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: /tags/rel-0_0_1/archive/scripts/src/phase2/configure.ac
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/configure.ac	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/configure.ac	(revision 7462)
@@ -0,0 +1,27 @@
+AC_PREREQ(2.59)
+
+AC_INIT([phase2], [0.0.1], [price@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([phase2.config])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+dnl AM_CONFIG_HEADER([config.h])
+AM_MAINTAINER_MODE
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC
+AC_PROG_INSTALL
+dnl AC_PROG_LIBTOOL
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 0.9.0]) 
+
+CFLAGS="${CFLAGS} -DTESTING"
+dnl is this the best was to setup recursive CFLAGS?
+dnl phase2_CFLAGS="-Werror -std=c99 "
+phase2_CFLAGS="-std=c99 "
+AC_SUBST([phase2_CFLAGS])
+
+AC_CONFIG_FILES([
+  Makefile
+])
+AC_OUTPUT
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/gpc1_raw.config
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/gpc1_raw.config	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/gpc1_raw.config	(revision 7462)
@@ -0,0 +1,172 @@
+# The raw GPC data comes off the telescope with each of the chips stored in separate files
+
+# How to identify this type
+RULE	METADATA
+#	TELESCOP	STR	PS1
+#	DETECTOR	STR	GPC1
+	EXTEND		BOOL	T
+	NEXTEND		S32	64
+	NAMPS		S32	64
+END
+
+# How to read this data
+PHU		STR	CHIP	# The FITS file represents a single chip
+EXTENSIONS	STR	CELL	# The extensions represent cells
+
+# What's in the FITS file?
+CONTENTS	METADATA
+	# Extension name, type
+	xy00	STR	pitch10u
+	xy01	STR	pitch10u
+	xy02	STR	pitch10u
+	xy03	STR	pitch10u
+	xy04	STR	pitch10u
+	xy05	STR	pitch10u
+	xy06	STR	pitch10u
+	xy07	STR	pitch10u
+	xy10	STR	pitch10u
+	xy11	STR	pitch10u
+	xy12	STR	pitch10u
+	xy13	STR	pitch10u
+	xy14	STR	pitch10u
+	xy15	STR	pitch10u
+	xy16	STR	pitch10u
+	xy17	STR	pitch10u
+	xy20	STR	pitch10u
+	xy21	STR	pitch10u
+	xy22	STR	pitch10u
+	xy23	STR	pitch10u
+	xy24	STR	pitch10u
+	xy25	STR	pitch10u
+	xy26	STR	pitch10u
+	xy27	STR	pitch10u
+	xy30	STR	pitch10u
+	xy31	STR	pitch10u
+	xy32	STR	pitch10u
+	xy33	STR	pitch10u
+	xy34	STR	pitch10u
+	xy35	STR	pitch10u
+	xy36	STR	pitch10u
+	xy37	STR	pitch10u
+	xy40	STR	pitch10u
+	xy41	STR	pitch10u
+	xy42	STR	pitch10u
+	xy43	STR	pitch10u
+	xy44	STR	pitch10u
+	xy45	STR	pitch10u
+	xy46	STR	pitch10u
+	xy47	STR	pitch10u
+	xy50	STR	pitch10u
+	xy51	STR	pitch10u
+	xy52	STR	pitch10u
+	xy53	STR	pitch10u
+	xy54	STR	pitch10u
+	xy55	STR	pitch10u
+	xy56	STR	pitch10u
+	xy57	STR	pitch10u
+	xy60	STR	pitch10u
+	xy61	STR	pitch10u
+	xy62	STR	pitch10u
+	xy63	STR	pitch10u
+	xy64	STR	pitch10u
+	xy65	STR	pitch10u
+	xy66	STR	pitch10u
+	xy67	STR	pitch10u
+	xy70	STR	pitch10u
+	xy71	STR	pitch10u
+	xy72	STR	pitch10u
+	xy73	STR	pitch10u
+	xy74	STR	pitch10u
+	xy75	STR	pitch10u
+	xy76	STR	pitch10u
+	xy77	STR	pitch10u
+END
+
+# Specify the cell data
+CELLS	METADATA
+	pitch10u	METADATA
+		CELL.BIASSEC.SOURCE	STR	VALUE
+		CELL.TRIMSEC.SOURCE	STR	VALUE
+		CELL.BIASSEC		STR	[575:606,1:594]
+		CELL.TRIMSEC		STR	[1:574,1:594]
+	#	CELL.BIASSEC		STR	BIASSEC
+	#	CELL.TRIMSEC		STR	DATASEC
+	END
+
+	# This is just in here for fun
+	pitch12u	METADATA
+		CELL.BIASSEC.SOURCE	STR	VALUE
+		CELL.TRIMSEC.SOURCE	STR	VALUE
+		CELL.BIASSEC		STR	[1:10,1:512];[523:574,1:512]
+		CELL.TRIMSEC		STR	[11:522,1:512]
+	#	CELL.BIASSEC		STR	BIASSEC
+	#	CELL.TRIMSEC		STR	TRIMSEC
+	END
+END
+
+
+# How to translate PS concepts into FITS headers
+TRANSLATION	METADATA
+	CELL.XBIN	STR	CCDSUM
+	CELL.YBIN	STR	CCDSUM
+	CELL.X0		STR	IMNPIX1
+	CELL.Y0		STR	IMNPIX2
+	CELL.XPARITY	STR	LTM1_1
+	CELL.YPARITY	STR	LTM2_2
+	CELL.SATURATION	STR	SATURATE
+END
+
+# Default PS concepts that may be specified by value
+DEFAULTS	METADATA
+	FPA.AIRMASS	F32	0.0
+	FPA.FILTER	STR	NONE
+	FPA.POSANGLE	F32	0.0
+	FPA.RA		STR	0:0:0
+	FPA.DEC		STR	0:0:0
+	FPA.RADECSYS	STR	ICRS
+	FPA.NAME	S32	12345
+	CELL.EXPOSURE	F32	0.0
+	CELL.DARKTIME	F32	0.0
+	CELL.GAIN	F32	1.0
+	CELL.READNOISE	F32	0.0
+	CELL.READDIR	S32	2
+	CELL.BAD	S32	0
+	CELL.TIMESYS	STR	UTC
+	CELL.TIME	STR	2005-11-23T12:34:56.78
+END
+
+# How to translation PS concepts into database lookups
+DATABASE	METADATA
+	TYPE		dbEntry		TABLE		COLUMN		GIVENDBCOL	GIVENPS
+#	CELL.GAIN	dbEntry		Camera		gain		chipId,cellId	CHIP,CELL
+#	CELL.READNOISE	dbEntry		Camera		readNoise	chipId,cellId	CHIP,CELL
+
+# A database entry refers to a particular column (COLUMN) in a
+# particular table (TABLE), given certain PS concepts (GIVENPS) that
+# match certain database columns (GIVENDBCOL).
+
+END
+
+
+# Where there might be some ambiguity, specify the format
+FORMATS		METADATA
+	FPA.RA		STR	HOURS
+	FPA.DEC		STR	DEGREES
+	CELL.TIME	STR	ISO
+	CELL.BINNING	STR	TOGETHER
+	CELL.X0		STR	FORTRAN
+	CELl.Y0		STR	FORTRAN
+END
+ 
+# Recipe options
+RECIPES		METADATA
+	PHASE2		STR	phase2.config		# Phase 2 recipe details
+END
+ 
+# How to get the supplementary stuff: mask and weight
+SUPPLEMENTARY	METADATA
+	MASK.SOURCE	STR	FILE		# Source type for mask: EXT | FILE
+	MASK.NAME	STR	%a_mask.fits	# Name for mask extension or filename
+	WEIGHT.SOURCE	STR	FILE		# Source type for weight: EXT | FILE
+	WEIGHT.NAME	STR	%a_weight.fits	# Name for weight extension or filename
+END
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/ipprc.config
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/ipprc.config	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/ipprc.config	(revision 7462)
@@ -0,0 +1,35 @@
+### Example .ipprc file
+
+### Database configuration
+DBSERVER	STR	ippdb.ifa.hawaii.edu	# Database host name (for psDBInit)
+DBUSER		STR	ipp			# Database user name (for psDBInit)
+DBPASSWORD	STR	password		# Database password (for psDBInit)
+
+### Setups for each camera system
+CAMERAS		METADATA
+	MEGACAM_RAW	STR	megacam_raw.config
+	GPC1_RAW	STR	gpc1_raw.config
+	MEGACAM_SPLICE	STR	megacam_splice.config
+#	LRIS_BLUE	STR	lris_blue.config
+#	LRIS_RED	STR	lris_red.config
+END
+
+### psLib setup
+#TIME		STR	/home/mithrandir/price/pan-starrs/jhroot/i686-pc-linux-gnu/etc/pslib/psTime.config	# Time configuration file
+LOGLEVEL	S32	3			# Logging level; 3=INFO
+LOGFORMAT	STR	THLNM			# Log format
+LOGDEST	STR	STDOUT				# Log destination
+TRACE		METADATA			# Trace levels
+	pap			S32	10
+	pmFPAPrint		S32	10
+	pmFPAWrite		S32	10
+#	pmConfigRead		S32	10
+#	pmFPAWriteMask		S32	10
+#	pmFPAWriteWeight	S32	10
+	pmChipMosaic		S32	10
+	pmFPAMorph		S32	10
+	spliceCells		S32	10
+	spliceImage		S32	10
+	setConceptItem		S32	10
+#	pap_psMetadataCopy	S32	9
+END
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/megacam_raw.config
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/megacam_raw.config	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/megacam_raw.config	(revision 7462)
@@ -0,0 +1,175 @@
+# The raw MegaCam data comes off the telescope with each of the chips stored in extensions of a MEF file.
+
+# How to identify this type
+RULE	METADATA
+	TELESCOP	STR	CFHT 3.6m
+	DETECTOR	STR	MegaCam
+	EXTEND		BOOL	T
+	NEXTEND		S32	72
+END
+
+# How to read this data
+PHU		STR	FPA	# The FITS file represents an entire FPA
+EXTENSIONS	STR	CELL	# The extensions represent cells
+
+# What's in the FITS file?
+CONTENTS	METADATA
+	# Extension name, chip name:type
+	amp00	STR	ccd00:left
+	amp01	STR	ccd00:right
+	amp02	STR	ccd01:left
+	amp03	STR	ccd01:right
+	amp04	STR	ccd02:left
+	amp05	STR	ccd02:right
+	amp06	STR	ccd03:left
+	amp07	STR	ccd03:right
+	amp08	STR	ccd04:left
+	amp09	STR	ccd04:right
+	amp10	STR	ccd05:left
+	amp11	STR	ccd05:right
+	amp12	STR	ccd06:left
+	amp13	STR	ccd06:right
+	amp14	STR	ccd07:left
+	amp15	STR	ccd07:right
+	amp16	STR	ccd08:left
+	amp17	STR	ccd08:right
+	amp18	STR	ccd09:left
+	amp19	STR	ccd09:right
+	amp20	STR	ccd10:left
+	amp21	STR	ccd10:right
+	amp22	STR	ccd11:left
+	amp23	STR	ccd11:right
+	amp24	STR	ccd12:left
+	amp25	STR	ccd12:right
+	amp26	STR	ccd13:left
+	amp27	STR	ccd13:right
+	amp28	STR	ccd14:left
+	amp29	STR	ccd14:right
+	amp30	STR	ccd15:left
+	amp31	STR	ccd15:right
+	amp32	STR	ccd16:left
+	amp33	STR	ccd16:right
+	amp34	STR	ccd17:left
+	amp35	STR	ccd17:right
+	amp36	STR	ccd18:left
+	amp37	STR	ccd18:right
+	amp38	STR	ccd19:left
+	amp39	STR	ccd19:right
+	amp40	STR	ccd20:left
+	amp41	STR	ccd20:right
+	amp42	STR	ccd21:left
+	amp43	STR	ccd21:right
+	amp44	STR	ccd22:left
+	amp45	STR	ccd22:right
+	amp46	STR	ccd23:left
+	amp47	STR	ccd23:right
+	amp48	STR	ccd24:left
+	amp49	STR	ccd24:right
+	amp50	STR	ccd25:left
+	amp51	STR	ccd25:right
+	amp52	STR	ccd26:left
+	amp53	STR	ccd26:right
+	amp54	STR	ccd27:left
+	amp55	STR	ccd27:right
+	amp56	STR	ccd28:left
+	amp57	STR	ccd28:right
+	amp58	STR	ccd29:left
+	amp59	STR	ccd29:right
+	amp60	STR	ccd30:left
+	amp61	STR	ccd30:right
+	amp62	STR	ccd31:left
+	amp63	STR	ccd31:right
+	amp64	STR	ccd32:left
+	amp65	STR	ccd32:right
+	amp66	STR	ccd33:left
+	amp67	STR	ccd33:right
+	amp68	STR	ccd34:left
+	amp69	STR	ccd34:right
+	amp70	STR	ccd35:left
+	amp71	STR	ccd35:right
+END
+
+# Specify the cell data
+CELLS	METADATA
+	left	METADATA	# Left amplifier
+		CELL.BIASSEC.SOURCE	STR	HEADER
+		CELL.TRIMSEC.SOURCE	STR	HEADER
+		CELL.BIASSEC		STR	BIASSEC
+		CELL.TRIMSEC		STR	DATASEC
+		CELL.XPARITY		S32	1 # We could have specified this as a DEFAULT, but this works
+		CELL.X0			S32	1
+		CELL.Y0			S32	1
+	END
+	right	METADATA	# Right amplifier
+		CELL.BIASSEC.SOURCE	STR	HEADER
+		CELL.TRIMSEC.SOURCE	STR	HEADER
+		CELL.BIASSEC		STR	BIASSEC
+		CELL.TRIMSEC		STR	DATASEC
+		CELL.XPARITY		S32	-1 # This cell is read out in the opposite direction
+		CELL.X0			S32	2048
+		CELL.Y0			S32	1
+	END
+END
+
+# How to translate PS concepts into FITS headers
+TRANSLATION	METADATA
+	FPA.NAME		STR	EXPNUM
+	FPA.AIRMASS		STR	AIRMASS
+	FPA.FILTER		STR	FILTER
+	FPA.POSANGLE		STR	ROTANGLE
+	FPA.RA			STR	RA
+	FPA.DEC			STR	DEC
+	FPA.RADECSYS		STR	RADECSYS
+	CELL.EXPOSURE		STR	EXPTIME
+	CELL.DARKTIME		STR	DARKTIME
+	CELL.GAIN		STR	GAIN
+	CELL.READNOISE		STR	RDNOISE
+	CELL.SATURATION		STR	SATURATE
+	CELL.TIME		STR	MJD-OBS
+	CELL.XBIN		STR	CCDBIN1
+	CELL.YBIN		STR	CCDBIN2
+END
+
+# Default PS concepts that may be specified by value
+DEFAULTS	METADATA
+	CELL.READDIR		S32	1		# Cell is read in x direction
+	CELL.BAD		S32	0
+	CELL.TIMESYS		STR	UTC
+	CELL.YPARITY		S32	1
+END
+
+# How to translation PS concepts into database lookups
+DATABASE	METADATA
+	TYPE		dbEntry		TABLE		COLUMN		GIVENDBCOL	GIVENPS
+#	CELL.GAIN	dbEntry		Camera		gain		chipId,cellId	CHIP.NAME,CELL.NAME
+#	CELL.READNOISE	dbEntry		Camera		readNoise	chipId,cellId	CHIP.NAME,CELL.NAME
+
+# A database entry refers to a particular column (COLUMN) in a
+# particular table (TABLE), given certain PS concepts (GIVENPS) that
+# match certain database columns (GIVENDBCOL).
+END
+
+
+# Where there might be some ambiguity, specify the format
+FORMATS		METADATA
+	FPA.RA		STR	HOURS
+	FPA.DEC		STR	DEGREES
+	CELL.TIME	STR	MJD
+#	CELL.BINNING	STR	SEPARATE
+	CELL.X0		STR	FORTRAN
+	CELL.Y0		STR	FORTRAN
+END
+
+# Recipe options
+RECIPES		METADATA
+	PHASE2		STR	phase2.config		# Phase 2 recipe details
+END
+
+
+# How to get the supplementary stuff: mask and weight
+SUPPLEMENTARY	METADATA
+	MASK.SOURCE	STR	FILE		# Source type for mask: EXT | FILE
+	MASK.NAME	STR	%a_mask.fits	# Name for mask extension or filename
+	WEIGHT.SOURCE	STR	FILE		# Source type for weight: EXT | FILE
+	WEIGHT.NAME	STR	%a_weight.fits	# Name for weight extension or filename
+END
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/nonlin.dat
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/nonlin.dat	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/nonlin.dat	(revision 7462)
@@ -0,0 +1,106 @@
+# Non-linearity correction lookup table
+# Col 1: Input value
+# Col 2: Corrected value
+0	0
+100	1
+200	2
+300	3
+400	4
+500	5
+600	6
+700	7
+800	8
+900	9
+1000	10
+1100	11
+1200	12
+1300	13
+1400	14
+1500	15
+1600	16
+1700	17
+1800	18
+1900	19
+2000	20
+2100	21
+2200	22
+2300	23
+2400	24
+2500	25
+2600	26
+2700	27
+2800	28
+2900	29
+3000	30
+3100	31
+3200	32
+3300	33
+3400	34
+3500	35
+3600	36
+3700	37
+3800	38
+3900	39
+4000	40
+4100	41
+4200	42
+4300	43
+4400	44
+4500	45
+4600	46
+4700	47
+4800	48
+4900	49
+5000	50
+5100	51
+5200	52
+5300	53
+5400	54
+5500	55
+5600	56
+5700	57
+5800	58
+5900	59
+6000	60
+6100	61
+6200	62
+6300	63
+6400	64
+6500	65
+6600	66
+6700	67
+6800	68
+6900	69
+7000	70
+7100	71
+7200	72
+7300	73
+7400	74
+7500	75
+7600	76
+7700	77
+7800	78
+7900	79
+8000	80
+8100	81
+8200	82
+8300	83
+8400	84
+8500	85
+8600	86
+8700	87
+8800	88
+8900	89
+9000	90
+9100	91
+9200	92
+9300	93
+9400	94
+9500	95
+9600	96
+9700	97
+9800	98
+9900	99
+10000	100
+10001	100
+1e6	100
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/papPhase2.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/papPhase2.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/papPhase2.c	(revision 7462)
@@ -0,0 +1,855 @@
+#include <stdio.h>
+#include <strings.h>
+
+#include "pslib.h"
+#include "psAdditionals.h"
+
+#include "pmFPA.h"
+#include "pmConfig.h"
+#include "pmFPAConstruct.h"
+#include "pmFPARead.h"
+#include "pmFPAConceptsGet.h"
+#include "pmFPAWrite.h"
+
+#include "pmFlatField.h"
+#include "pmMaskBadPixels.h"
+#include "pmNonLinear.h"
+#include "pmSubtractBias.h"
+#include "pmChipMosaic.h"
+//#include "pmFPAMorph.h"
+
+// Phase 2 needs to:
+// * Read configurations
+// * Read in the images
+// * Flag bad pixels
+// * Non-linearity correction
+// * Bias/dark subtraction (MHPCC)
+// * Flat-field (MHPCC)
+// * Fringe subtraction (Not for now)
+// * Source identification and photometry (IPP prototype)
+// * Astrometry (IPP prototype soon)
+// * Write calibrated image
+// * Write source catalogue, astrometry, etc.
+
+// Currently neglecting different input types (F32, S32, U16, etc)
+
+// Would like to fold in Nebulous at some stage as well.
+
+// Currently neglect to convolve the reference frames with the orthogonal transfer kernel
+
+// phase2 INPUT.fits OUTPUT.fits -bias BIAS.fits -dark DARK.fits -flat FLAT.fits -chip CHIP
+//
+// Most are self-explanatory.  "-chip" says "only work on this particular chip".
+
+
+#define RECIPE "PHASE2"                 // Name of the recipe to use
+
+static psMemId memPrintAlloc(const psMemBlock *mb)
+{
+    printf("Allocated memory block %ld (%ld):\n"
+           "\tFile %s, line %d, size %d\n"
+           "\tPosts: %x %x %x\n",
+           mb->id, mb->refCounter, mb->file, mb->lineno, mb->userMemorySize, mb->startblock, mb->endblock,
+           *(void**)((int8_t *)(mb + 1) + mb->userMemorySize));
+    return 0;
+}
+static psMemId memPrintFree(const psMemBlock *mb)
+{
+    printf("Freed memory block %ld (%ld):\n"
+           "\tFile %s, line %d, size %d\n"
+           "\tPosts: %x %x %x\n",
+           mb->id, mb->refCounter, mb->file, mb->lineno, mb->userMemorySize, mb->startblock, mb->endblock,
+           *(void**)((int8_t *)(mb + 1) + mb->userMemorySize));
+    return 0;
+}
+static void memPrint(const psPtr ptr)
+{
+    psMemBlock *mb = ((psMemBlock*)ptr) - 1;
+    printf("Memory block %ld (%ld):\n"
+           "\tFile %s, line %d, size %d\n"
+           "\tPosts: %x %x %x\n",
+           mb->id, mb->refCounter, mb->file, mb->lineno, mb->userMemorySize, mb->startblock, mb->endblock,
+           *(void**)((int8_t *)(mb + 1) + mb->userMemorySize));
+}
+
+int main(int argc, char *argv[])
+{
+#if 0
+    // Hunting memory leaks
+    psMemAllocCallbackSetID(22367);
+    psMemFreeCallbackSetID(22367);
+    psMemAllocCallbackSet(memPrintAlloc);
+    psMemFreeCallbackSet(memPrintFree);
+#endif
+
+    psTimerStart("phase2");
+
+    // Parse the configurations
+    psMetadata *site = NULL;            // Site configuration
+    psMetadata *camera = NULL;          // Camera configuration
+    psMetadata *recipe = NULL;          // Recipe configuration
+    if (! pmConfigRead(&site, &camera, &recipe, &argc, argv, RECIPE)) {
+        psErrorStackPrint(stderr, "Can't find site configuration!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // Parse other command-line arguments
+    psMetadata *arguments = psMetadataAlloc(); // The arguments, with default values
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-bias", 0, "Name of the bias image", "");
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-dark", 0, "Name of the dark image", "");
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-flat", 0, "Name of the flat-field image", "");
+    psMetadataAddStr(arguments, PS_LIST_TAIL, "-mask", 0, "Name of the mask image", "");
+    psMetadataAddS32(arguments, PS_LIST_TAIL, "-chip", 0, "Chip number to process (if positive)", -1);
+    if (! psArgumentParse(arguments, &argc, argv) || argc != 3) {
+        printf("\nPan-STARRS Phase 2 processing\n\n");
+        printf("Usage: %s INPUT.fits OUTPUT.fits\n\n", argv[0]);
+        psArgumentHelp(arguments);
+        psFree(arguments);
+        exit(EXIT_FAILURE);
+    }
+    const char *inputName = argv[1];    // Name of input image
+    const char *outputName = argv[2];   // Name of output image
+    const char *biasName = psMetadataLookupString(NULL, arguments, "-bias"); // Name of bias image
+    const char *darkName = psMetadataLookupString(NULL, arguments, "-dark"); // Name of dark image
+    const char *flatName = psMetadataLookupString(NULL, arguments, "-flat"); // Name of flat-field image
+    const char *maskName = psMetadataLookupString(NULL, arguments, "-mask"); // Name of mask image
+    const int chipNum = psMetadataLookupS32(NULL, arguments, "-chip"); // Chip number to work on
+    printf("Input: %s\nOutput: %s\n", inputName, outputName);
+    printf("Bias: %s\n", biasName);
+    printf("Dark: %s\n", darkName);
+    printf("Flat: %s\n", flatName);
+    printf("Mask: %s\n", maskName);
+    printf("Chip: %d\n", chipNum);
+
+    // Open the input
+    psFits *inputFile = psFitsOpen(inputName, "r"); // File handle for FITS file
+    if (! inputFile) {
+        psErrorStackPrint(stderr, "Can't open input image: %s\n", inputName);
+        exit(EXIT_FAILURE);
+    }
+    psMetadata *header = psFitsReadHeader(NULL, inputFile); // FITS header
+#if PRODUCTION
+    psDB *database = pmConfigDB(site);  // Database handle
+#else
+    psDB *database = NULL;              // Database handle
+#endif
+
+    // Open the output and output mask
+    // We do it here so that we don't process the whole lot and then find out we can't open the output file
+    psFits *outputFile = psFitsOpen(outputName, "w");
+    if (! outputFile) {
+        psErrorStackPrint(stderr, "Can't open output image: %s\n", outputName);
+        exit(EXIT_FAILURE);
+    }
+
+    // Get camera configuration from header if not already defined
+    if (! camera) {
+        camera = pmConfigCameraFromHeader(site, header);
+        if (! camera) {
+            psErrorStackPrint(stderr, "Can't find camera configuration!\n");
+            exit(EXIT_FAILURE);
+        }
+   } else if (! pmConfigValidateCamera(camera, header)) {
+        psError(PS_ERR_IO, true, "%s does not seem to be from the camera.\n", inputName);
+        exit(EXIT_FAILURE);
+    }
+    if (! recipe && !(recipe = pmConfigRecipeFromCamera(camera, RECIPE))) {
+        psErrorStackPrint(stderr, "Can't find recipe configuration!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // Construct camera in preparation for reading
+    pmFPA *input = pmFPAConstruct(camera);
+    pmFPA *mask = pmFPAConstruct(camera);
+    pmFPA *bias = pmFPAConstruct(camera);
+    pmFPA *dark = pmFPAConstruct(camera);
+    pmFPA *flat = pmFPAConstruct(camera);
+
+    // Set various tasks
+    bool doMask = false;                // Mask bad pixles
+    bool doNonLin = false;              // Non-linearity correction
+    bool doBias = false;                // Bias subtraction
+    bool doDark = false;                // Dark subtraction
+    bool doOverscan = false;            // Overscan subtraction
+    bool doFlat = false;                // Flat-field normalisation
+    bool doFringe = false;              // Fringe subtraction
+    bool doSource = false;              // Source identification and photometry
+    bool doAstrom = false;              // Astrometry
+    pmOverscanAxis overscanMode = PM_OVERSCAN_NONE; // Axis for overscan
+    pmFit overscanFitType = PM_FIT_NONE; // Fit type for overscan
+    int overscanBins = 1;               // Number of pixels per bin for overscan
+    psStats *overscanStats = NULL;      // Statistics for overscan
+    void *overscanFit = NULL;           // Overscan fit (polynomial or spline)
+
+    if (psMetadataLookupBool(NULL, recipe, "MASK")) {
+        if (strlen(maskName) > 0) {
+            doMask = true;
+        } else {
+            psLogMsg("phase2", PS_LOG_WARN, "Masking is desired, but no mask was supplied --- no masking "
+                     "will be performed.\n");
+        }
+    }
+    if (psMetadataLookupBool(NULL, recipe, "NONLIN")) {
+        doNonLin = true;
+    }
+    if (psMetadataLookupBool(NULL, recipe, "BIAS")) {
+        if (strlen(biasName) > 0) {
+            doBias = true;
+        } else {
+            psLogMsg("phase2", PS_LOG_WARN, "Bias subtraction is desired, but no bias was supplied --- "
+                     "no bias subtraction will be performed.\n");
+        }
+    }
+    if (psMetadataLookupBool(NULL, recipe, "DARK")) {
+        if (strlen(darkName) > 0) {
+            doDark = true;
+        } else {
+            psLogMsg("phase2", PS_LOG_WARN, "Dark subtraction is desired, but no dark was supplied --- "
+                     "no dark subtraction will be performed.\n");
+        }
+    }
+    if (psMetadataLookupBool(NULL, recipe, "OVERSCAN")) {
+        doOverscan = true;
+        psString mode = psMetadataLookupString(NULL, recipe, "OVERSCAN.MODE");
+        if (strcasecmp(mode, "INDIVIDUAL") == 0) {
+            overscanMode = PM_OVERSCAN_ROWS;
+            // By "ROWS", I mean either rows or columns --- will check the READDIR for each chip
+        } else if (strcasecmp(mode, "ALL") == 0) {
+            overscanMode = PM_OVERSCAN_ALL;
+        } else if (strcasecmp(mode, "NONE") != 0) {
+            psLogMsg("phase2", PS_LOG_WARN, "OVERSCAN.MODE (%s) is not one of NONE, INDIVIDUAL, or ALL:"
+                     " assuming NONE.\n", mode);
+        }
+        psString fit = psMetadataLookupString(NULL, recipe, "OVERSCAN.FIT");
+        if (strcasecmp(fit, "POLYNOMIAL") == 0) {
+            overscanFitType = PM_FIT_POLYNOMIAL;
+            int order = psMetadataLookupS32(NULL, recipe, "OVERSCAN.ORDER"); // Order of polynomial fit
+            overscanFit = psPolynomial1DAlloc(order, PS_POLYNOMIAL_ORD);
+        } else if (strcasecmp(fit, "SPLINE") == 0) {
+            overscanFitType = PM_FIT_SPLINE;
+            int order = psMetadataLookupS32(NULL, recipe, "OVERSCAN.ORDER"); // Order of polynomial fit
+            overscanFit = NULL;
+//          overscanFit = psSpline1DAlloc();
+        } else if (strcasecmp(fit, "NONE") != 0) {
+            psLogMsg("phase2", PS_LOG_WARN, "OVERSCAN.FIT (%s) is not one of NONE, POLYNOMIAL, or SPLINE:"
+                     " assuming NONE.\n", fit);
+        }
+        overscanBins = psMetadataLookupS32(NULL, recipe, "OVERSCAN.BIN");
+        if (overscanBins <= 0) {
+            psErrorStackPrint(stderr, "OVERSCAN.BIN (%d) is non-positive --- assuming 1.\n", overscanBins);
+            overscanBins = 1;
+        }
+        psString stat = psMetadataLookupString(NULL, recipe, "OVERSCAN.STAT");
+        if (strcasecmp(stat, "MEAN")) {
+            overscanStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        } else if (strcasecmp(stat, "MEDIAN")) {
+            overscanStats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+        } else {
+            psErrorStackPrint(stderr, "OVERSCAN.STAT (%s) is not one of MEAN, MEDIAN: assuming MEAN\n", stat);
+            overscanStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        }
+    }
+
+    if (psMetadataLookupBool(NULL, recipe, "FLAT")) {
+        if (strlen(flatName) > 0) {
+            doFlat = true;
+        } else {
+            psLogMsg("phase2", PS_LOG_WARN, "Flat-fielding is desired, but no flat was supplied --- "
+                     "no flat-fielding will be performed.\n");
+        }
+    }
+
+    // Chip selection
+    if (chipNum >= 0) {
+        if (! pmFPASelectChip(input, chipNum) || ! pmFPASelectChip(bias, chipNum) ||
+            ! pmFPASelectChip(dark, chipNum) || ! pmFPASelectChip(flat, chipNum) ||
+            ! pmFPASelectChip(mask, chipNum)) {
+            psErrorStackPrint(stderr, "Chip number %d doesn't exist in camera.\n", chipNum);
+            exit(EXIT_FAILURE);
+        }
+        psLogMsg("phase2", PS_LOG_INFO, "Operating only on chip %d\n", chipNum);
+    }
+
+    psLogMsg("phase2", PS_LOG_INFO, "Setup completed after %f sec.\n", psTimerMark("phase2"));
+
+    // Read in the input pixels
+    psLogMsg("phase2", PS_LOG_INFO, "Opening input image: %s\n", inputName);
+    if (! pmFPARead(input, inputFile, header, database)) {
+        psErrorStackPrint(stderr, "Unable to populate camera from FITS file: %s\n", inputName);
+        exit(EXIT_FAILURE);
+    }
+    psFitsClose(inputFile);
+
+#if 0
+    pmFPAReadMask(input, inputFile);
+    pmFPAReadWeight(input, inputFile);
+//#else
+    {
+        // Generate mask and weight frame
+        p_pmHDU *hdu = NULL;
+        if (input->hdu) {
+            hdu = input->hdu;
+        }
+        psArray *chips = input->chips;
+        for (int chipNum = 0; chipNum < chips->n; chipNum++) {
+            pmChip *chip = chips->data[chipNum];
+            if (chip->valid) {
+                if (chip->hdu) {
+                    hdu = chip->hdu;
+                }
+                psArray *cells = chip->cells;
+                for (int cellNum = 0; cellNum < cells->n; cellNum++) {
+                    pmCell *cell = cells->data[cellNum];
+                    if (cell->valid) {
+                        if (cell->hdu) {
+                            hdu = cell->hdu;
+                        }
+
+                        hdu->masks = psArrayAlloc(hdu->images->n);
+                        hdu->weights = psArrayAlloc(hdu->images->n);
+                        psArray *readouts = cell->readouts;
+                        for (int readNum = 0; readNum < hdu->images->n; readNum++) {
+                            psImage *image = hdu->images->data[readNum];
+                            psImage *mask = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+                            psImage *weight = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+                            pmReadout *readout = readouts->data[readNum];
+                            for (int j = 0; j < image->numRows; j++) {
+                                for (int i = 0; i < image->numCols; i++) {
+                                    mask->data.U8[j][i] = j + i;
+                                    weight->data.F32[j][i] = sqrtf(image->data.F32[j][i]);
+                                }
+                            }
+                            hdu->masks->data[readNum] = mask;
+                            hdu->weights->data[readNum] = weight;
+                            readout->mask = psMemIncrRefCounter(mask);
+                            readout->weight = psMemIncrRefCounter(weight);
+                        }
+                    }
+                }
+            }
+        }
+    }
+#endif
+
+#if 0
+    pmFPAPrint(input);
+#endif
+
+    // Load the calibration frames, if required
+    if (doBias) {
+        psFits *biasFile = psFitsOpen(biasName, "r"); // File handle for bias
+        psMetadata *biasHeader = psFitsReadHeader(NULL, biasFile); // FITS header for bias
+        if (! pmConfigValidateCamera(camera, biasHeader)) {
+            psError(PS_ERR_IO, true, "Bias (%s) does not seem to be from the same camera.\n", biasName);
+            exit(EXIT_FAILURE);
+        }
+        psLogMsg("phase2", PS_LOG_INFO, "Opening bias image: %s\n", biasName);
+        if (! pmFPARead(bias, biasFile, biasHeader, database)) {
+            psErrorStackPrint(stderr, "Unable to populate bias camera from fits FITS: %s\n", biasName);
+            exit(EXIT_FAILURE);
+        }
+        psFree(biasHeader);
+        psFitsClose(biasFile);
+    }
+
+    if (doDark) {
+        psFits *darkFile = psFitsOpen(darkName, "r"); // File handle for dark
+        psMetadata *darkHeader = psFitsReadHeader(NULL, darkFile); // FITS header for dark
+        if (! pmConfigValidateCamera(camera, darkHeader)) {
+            psError(PS_ERR_IO, true, "Dark (%s) does not seem to be from the same camera.\n", darkName);
+            exit(EXIT_FAILURE);
+        }
+        psLogMsg("phase2", PS_LOG_INFO, "Opening dark image: %s\n", darkName);
+        if (! pmFPARead(dark, darkFile, darkHeader, database)) {
+            psErrorStackPrint(stderr, "Unable to populate dark camera from fits FITS: %s\n", darkName);
+            exit(EXIT_FAILURE);
+        }
+        psFree(darkHeader);
+        psFitsClose(darkFile);
+    }
+
+    if (doMask) {
+        psFits *maskFile = psFitsOpen(maskName, "r"); // File handle for mask
+        psMetadata *maskHeader = psFitsReadHeader(NULL, maskFile); // FITS header for mask
+        if (! pmConfigValidateCamera(camera, maskHeader)) {
+            psError(PS_ERR_IO, true, "Mask (%s) does not seem to be from the same camera.\n", maskName);
+            exit(EXIT_FAILURE);
+        }
+        psLogMsg("phase2", PS_LOG_INFO, "Opening mask image: %s\n", maskName);
+        if (! pmFPARead(mask, maskFile, maskHeader, database)) {
+            psErrorStackPrint(stderr, "Unable to populate mask camera from fits FITS: %s\n", maskName);
+            exit(EXIT_FAILURE);
+        }
+        psFree(maskHeader);
+        psFitsClose(maskFile);
+    }
+
+    if (doFlat) {
+        psFits *flatFile = psFitsOpen(flatName, "r"); // File handle for flat
+        if (! flatFile) {
+            psErrorStackPrint(stderr, "Can't open flat image: %s\n", flatName);
+            exit(EXIT_FAILURE);
+        }
+        psMetadata *flatHeader = psFitsReadHeader(NULL, flatFile); // FITS header for flat
+        if (! pmConfigValidateCamera(camera, flatHeader)) {
+            psError(PS_ERR_IO, true, "Flat (%s) does not seem to be from the same camera.\n", flatName);
+            exit(EXIT_FAILURE);
+        }
+        psLogMsg("phase2", PS_LOG_INFO, "Opening flat image: %s\n", flatName);
+        if (! pmFPARead(flat, flatFile, flatHeader, database)) {
+            psErrorStackPrint(stderr, "Unable to populate flat camera from fits FITS: %s\n", flatName);
+            exit(EXIT_FAILURE);
+        }
+        psFree(flatHeader);
+        psFitsClose(flatFile);
+    }
+
+    psLogMsg("phase2", PS_LOG_INFO, "Input completed after %f sec.\n", psTimerMark("phase2"));
+
+    psArray *inputChips = input->chips; // Array of chips in input image
+    psArray *biasChips = bias->chips;   // Array of chips in bias image
+    psArray *darkChips = dark->chips;   // Array of chips in dark image
+    psArray *maskChips = mask->chips;   // Array of chips in mask image
+    psArray *flatChips = flat->chips;   // Array of chips in flat image
+    for (int i = 0; i < inputChips->n; i++) {
+        pmChip *inputChip = inputChips->data[i]; // Chip of interest in input image
+        pmChip *biasChip = biasChips->data[i]; // Chip of interest in bias image
+        pmChip *darkChip = darkChips->data[i]; // Chip of interest in dark image
+        pmChip *maskChip = maskChips->data[i]; // Chip of interest in mask image
+        pmChip *flatChip = flatChips->data[i]; // Chip of interest in flat image
+
+        if (! inputChip->valid) {
+            continue;
+        }
+
+        psArray *inputCells = inputChip->cells; // Array of cells in input image
+        psArray *biasCells = biasChip->cells; // Array of cells in bias image
+        psArray *darkCells = darkChip->cells; // Array of cells in dark image
+        psArray *maskCells = maskChip->cells; // Array of cells in mask image
+        psArray *flatCells = flatChip->cells; // Array of cells in flat image
+
+        for (int j = 0; j < inputCells->n; j++) {
+            pmCell *inputCell = inputCells->data[j]; // Cell of interest in input image
+            pmCell *biasCell = biasCells->data[j]; // Cell of interest in bias image
+            pmCell *darkCell = darkCells->data[j]; // Cell of interest in dark imag
+            pmCell *maskCell = maskCells->data[j]; // Cell of interest in mask image
+            pmCell *flatCell = flatCells->data[j]; // Cell of interest in flat image
+
+            if (! inputCell->valid) {
+                continue;
+            }
+
+            psArray *inputReadouts = inputCell->readouts; // Array of readouts in input image
+            if (doBias && biasCell->readouts->n > 1) {
+                psLogMsg("phase2", PS_LOG_WARN, "Bias contains multiple readouts: only the first will be"
+                         " used.");
+            }
+            if (doDark && darkCell->readouts->n > 1) {
+                psLogMsg("phase2", PS_LOG_WARN, "Dark contains multiple readouts: only the first will be"
+                         " used.");
+            }
+            if (doMask && maskCell->readouts->n > 1) {
+                psLogMsg("phase2", PS_LOG_WARN, "Mask contains multiple readouts: only the first will be"
+                         " used.");
+            }
+            if (doFlat && flatCell->readouts->n > 1) {
+                psLogMsg("phase2", PS_LOG_WARN, "Flat contains multiple readouts: only the first will be"
+                         " used.");
+            }
+
+            pmReadout *biasReadout = NULL; // Bias readout
+            pmReadout *maskReadout = NULL; // Mask readout
+            pmReadout *flatReadout = NULL; // Flat readout
+            psImage *biasImage = NULL;  // Bias pixels
+            psImage *darkImage = NULL;  // Dark pixels
+            float darkTime = 1.0;       // Dark time for dark image
+            psImage *maskImage = NULL;  // Mask pixels
+
+            if (doBias) {
+                biasReadout = biasCell->readouts->data[0]; // Readout of interest in bias image
+                biasImage = biasReadout->image;
+            }
+            if (doDark) {
+                pmReadout *darkReadout = darkCell->readouts->data[0]; // Readout of interest in dark image
+                darkImage = darkReadout->image;
+                darkTime = psMetadataLookupF32(NULL, darkCell->concepts, "CELL.DARKTIME");
+                if (darkTime <= 0.0) {
+                    psErrorStackPrint(stderr, "DARKTIME for dark image (%f) is non-positive.\n", darkTime);
+                    exit(EXIT_FAILURE);
+                }
+            }
+            if (doMask) {
+                maskReadout = maskCell->readouts->data[0]; // Readout of interest in mask image
+                maskImage = maskReadout->image;
+            }
+            if (doFlat) {
+                flatReadout = flatCell->readouts->data[0]; // Readout of interest in mask image
+            }
+
+            for (int k = 0; k < inputReadouts->n; k++) {
+                pmReadout *inputReadout = inputReadouts->data[k]; // Readout of interest in input image
+                psImage *inputImage = inputReadout->image; // The actual image
+
+                // Mask bad pixels
+                if (doMask) {
+                    float saturation = psMetadataLookupF32(NULL, inputCell->concepts, "CELL.SATURATION");
+
+                    // Need to change this later to grow the mask by the size of the convolution kernel
+                    (void)pmMaskBadPixels(inputReadout, maskImage,
+                                          PM_MASK_TRAP | PM_MASK_BADCOL | PM_MASK_SAT, saturation,
+                                          PM_MASK_TRAP, 0);
+                }
+
+                // Non-linearity correction
+                if (doNonLin) {
+                    psMetadataItem *dataItem = psMetadataLookup(recipe, "NONLIN.DATA");
+                    if (! dataItem) {
+                        psLogMsg("phase2", PS_LOG_WARN, "Non-linearity correction desired, but unable to "
+                                 "find NONLIN.DATA in recipe --- ignored.\n");
+                    } else {
+                        switch (dataItem->type) {
+                          case PS_DATA_VECTOR:
+                            {
+                                // These are the polynomial coefficients
+                                psVector *coeff = dataItem->data.V; // The coefficient vector
+                                if (coeff->type.type != PS_TYPE_F64) {
+                                    psVector *temp = psVectorCopy(NULL, coeff, PS_TYPE_F64); // F64 version
+                                    coeff = temp;
+                                }
+                                psPolynomial1D *correction = psPolynomial1DAlloc(coeff->n - 1,
+                                                                                 PS_POLYNOMIAL_ORD);
+                                psFree(correction->coeff);
+                                correction->coeff = psMemIncrRefCounter(coeff->data.F64);
+                                (void)pmNonLinearityPolynomial(inputReadout, correction);
+                                psFree(coeff);
+                                psFree(correction);
+                            }
+                            break;
+                          case PS_DATA_STRING:
+                            {
+                                // This is a filename
+                                psString name = dataItem->data.V;       // Filename
+                                psLookupTable *table = psLookupTableAlloc(name, "%f %f", 0);
+                                if (psLookupTableRead(table) <= 0) {
+                                    psErrorStackPrint(stderr, "Unable to read non-linearity correction file "
+                                                      "%s --- ignored\n", name);
+                                } else {
+#ifdef PRODUCTION
+                                    (void)pmNonLinearityLookup(inputReadout, table);
+#else
+                                    psVector *influx = table->values->data[0];
+                                    psVector *outflux = table->values->data[1];
+                                    (void)pmNonLinearityLookup(inputReadout, table->values->data[0],
+                                                               table->values->data[1]);
+#endif
+                                }
+                                psFree(table);
+                            }
+                            break;
+                          case PS_DATA_METADATA:
+                            {
+                                // This is a menu
+                                psMetadata *options = dataItem->data.V; // Options with concept values as keys
+                                bool mdok = false; // Success of MD lookup
+                                psString concept = psMetadataLookupString(&mdok, recipe, "NONLIN.SOURCE");
+                                if (! mdok || ! concept) {
+                                    psLogMsg("phase2", PS_LOG_WARN, "Non-linearity correction desired, but "
+                                             "unable to find NONLIN.SOURCE in recipe --- ignored.\n");
+                                } else {
+                                    psMetadataItem *conceptValueItem = pmCellGetConcept(inputCell, concept);
+                                    if (! conceptValueItem) {
+                                        psLogMsg("phase2", PS_LOG_WARN, "Unable to find value of concept %s "
+                                                 "for non-linearity correction --- ignored.\n", concept);
+                                    } else if (conceptValueItem->type != PS_DATA_STRING) {
+                                        psLogMsg("phase2", PS_LOG_WARN, "Type for concept %s isn't STRING, as"
+                                                 " expected for non-linearity correction --- ignored.\n",
+                                                 concept);
+                                    } else {
+                                        psString conceptValue = conceptValueItem->data.V;
+                                        // Get the value of the concept
+                                        psMetadataItem *optionItem = psMetadataLookup(options, conceptValue);
+                                        if (!optionItem) {
+                                            psLogMsg("phase2", PS_LOG_WARN, "Unable to find %s in NONLIN.DATA"
+                                                     " --- ignored.\n", conceptValue);
+                                        } else if (optionItem->type == PS_DATA_VECTOR) {
+                                            // These are the polynomial coefficients
+                                            psVector *coeff = optionItem->data.V; // The coefficient vector
+                                            if (coeff->type.type != PS_TYPE_F64) {
+                                                psVector *temp = psVectorCopy(NULL, coeff, PS_TYPE_F64);
+                                                coeff = temp;
+                                            }
+                                            // Polynomial correction
+                                            psPolynomial1D *correction = psPolynomial1DAlloc(coeff->n - 1,
+                                                                                             PS_POLYNOMIAL_ORD);
+                                            psFree(correction->coeff);
+                                            correction->coeff = psMemIncrRefCounter(coeff->data.F64);
+                                            (void)pmNonLinearityPolynomial(inputReadout, correction);
+                                            psFree(coeff);
+                                            psFree(correction);
+                                        } else if (optionItem->type == PS_DATA_STRING) {
+                                            // This is a filename
+                                            psString tableName = optionItem->data.V; // The filename
+                                            psLookupTable *table = psLookupTableAlloc(tableName, "%f %f", 0);
+                                            int numLines = 0; // Number of lines read from table
+                                            if ((numLines = psLookupTableRead(table)) <= 0) {
+                                                psErrorStackPrint(stderr, "Unable to read non-linearity "
+                                                                  "correction file %s --- ignored\n",
+                                                                  tableName);
+                                            } else {
+#ifdef PRODUCTION
+                                                (void)pmNonLinearityLookup(inputReadout, table);
+#else
+                                                printf("XXX: Non-linearity correction from lookup table not "
+                                                       "yet implemented.\n");
+#endif
+                                            }
+                                            psFree(table);
+                                        } else {
+                                            psLogMsg("phase2", PS_LOG_WARN, "Non-linearity correction "
+                                                     "desired but unable to interpret NONLIN.DATA for %s"
+                                                     " --- ignored\n", conceptValue);
+                                        }
+                                    }
+                                }
+                            }
+                            break;
+                          default:
+                            psLogMsg("phase2", PS_LOG_WARN, "Non-linearity correction desired, but "
+                                     "NONLIN.DATA is of invalid type --- ignored.\n");
+                        }
+                    }
+                } // Non-linearity correction
+
+                // Bias, dark and overscan subtraction are all merged.
+
+                // Changed the definition of pmSubtractBias to do the dark subtraction as well, but
+                // it's not in there yet.  Once it is, we can get rid of "pedestal".
+
+#ifdef PRODUCTION
+                psImage *pedestal = NULL;       // Pedestal image (bias + scaled dark)
+                if (doDark) {
+                    // Dark time for input image
+                    float inputTime = psMetadataLookupF32(NULL, inputCell->concepts, "CELL.DARKTIME");
+                    if (inputTime <= 0) {
+                        psErrorStackPrint(stderr, "DARKTIME for input image (%f) is non-positive.\n",
+                                          inputTime);
+                        exit(EXIT_FAILURE);
+                    }
+
+                    pedestal = (psImage*)psBinaryOp(NULL, darkImage, "*",
+                                                    psScalarAlloc(inputTime * darkTime, PS_TYPE_F32));
+                }
+                if (doBias) {
+                    if (pedestal) {
+                        if (biasImage->numRows != darkImage->numRows ||
+                            biasImage->numCols != darkImage->numCols) {
+                            psError(PS_ERR_IO, true, "Bias and dark images have different dimensions: "
+                                    "%dx%d vs %dx%d\n", biasImage->numCols, biasImage->numRows,
+                                    darkImage->numCols, darkImage->numRows);
+                            exit(EXIT_FAILURE);
+                        }
+                        (void)psBinaryOp(pedestal, pedestal, "+", biasImage);
+                    } else {
+                        pedestal = psMemIncrRefCounter(biasImage);
+                    }
+                }
+#endif
+                if (overscanMode == PM_OVERSCAN_ROWS || overscanMode == PM_OVERSCAN_COLUMNS) {
+                    // Need to get the read direction
+                    int readdir = psMetadataLookupS32(NULL, inputCell->concepts, "CELL.READDIR");
+                    if (readdir == 1) {
+// psmodule-0.8.0 has confused PM_OVERSCAN_ROWS and PM_OVERSCAN_COLUMNS; bug 608.
+#ifdef PRODUCTION
+                        overscanMode = PM_OVERSCAN_ROWS;
+#else
+                        overscanMode = PM_OVERSCAN_COLUMNS;
+#endif
+                    } else if (readdir == 2) {
+#ifdef PRODUCTION
+                        overscanMode = PM_OVERSCAN_COLUMNS;
+#else
+                        overscanMode = PM_OVERSCAN_ROWS;
+#endif
+                    } else {
+                        psErrorStackPrint(stderr, "CELL.READDIR (%d) is not 1 or 2 --- assuming 1.\n",
+                                          readdir);
+                        overscanMode = PM_OVERSCAN_ROWS;
+                    }
+                }
+
+                if (doBias || doOverscan || doDark) {
+                    psList *inputOverscans = pmReadoutGetBias(inputReadout); // List of overscan bias regions
+#ifdef PRODUCTION
+                    (void)pmSubtractBias(inputReadout, overscanFit, inputOverscans, overscanMode,
+                                         overscanStats, overscanBins, overscanFitType, pedestal);
+#else
+                    (void)pmSubtractBias(inputReadout, overscanFit, inputOverscans, overscanMode,
+                                         overscanStats, overscanBins, overscanFitType, biasReadout);
+#endif
+                    psFree(inputOverscans);
+
+                    // Output overscan fit results, if required
+                    if (doOverscan) {
+                        if (! overscanFit) {
+                            psLogMsg("phase2", PS_LOG_WARN, "No fit generated!\n");
+                        } else {
+                            switch (overscanFitType) {
+                              case PM_FIT_POLYNOMIAL:
+                                {
+                                    psPolynomial1D *poly = (psPolynomial1D*)overscanFit; // The polynomial
+                                    psString coeffs = NULL;     // String containing the coefficients
+                                    for (int i = 0; i < poly->nX; i++) {
+                                        psStringAppend(&coeffs, "%e ", poly->coeff[i]);
+                                    }
+                                    psLogMsg("phase2", PS_LOG_INFO, "Overscan polynomial coefficients:\n%s\n",
+                                             coeffs);
+                                    psFree(coeffs);
+                                }
+                                break;
+                              case PM_FIT_SPLINE:
+                                {
+                                    psSpline1D *spline = (psSpline1D*)overscanFit; // The spline
+                                    psString coeffs = NULL;     // String containing the coefficients
+                                    for (int i = 0; i < spline->n; i++) {
+                                        psPolynomial1D *poly = spline->spline[i]; // i-th polynomial
+                                        psStringAppend(&coeffs, "%d: ", i);
+                                        for (int j = 0; j < poly->nX; j++) {
+                                            psStringAppend(&coeffs, "%e ", poly->coeff[i]);
+                                        }
+                                        psStringAppend(&coeffs, "\n");
+                                    }
+                                    psLogMsg("phase2", PS_LOG_INFO, "Overscan spline coefficients:\n%s\n",
+                                             coeffs);
+                                    psFree(coeffs);
+                                }
+                                break;
+                              case PM_FIT_NONE:
+                                break;
+                              default:
+                                psAbort(__func__, "Should never get here!!!\n");
+                            }
+                        }
+                    }
+                }
+
+                // XXX: Temporary: until the other functions are altered to do this themselves.
+                // Trim, so that flat, fringe etc computations are faster.
+#if 0
+#ifndef PRODUCTION
+                psRegion *trimRegion = psMetadataLookupPtr(NULL, inputCell->concepts, "CELL.TRIMSEC");
+                psImage *trimmed = psImageSubset(inputReadout->image, *trimRegion);
+                psFree(inputReadout->image);
+                inputReadout->image = trimmed;
+#endif
+#endif
+                // Flat-field correction
+                if (doFlat) {
+                    psLogMsg("phase2", PS_LOG_INFO, "Performing flat field.\n");
+#ifndef PRODUCTION
+                    psImage *dummyImage = psImageAlloc(inputReadout->image->numCols,
+                                                       inputReadout->image->numRows,
+                                                       PS_TYPE_U8);
+                    pmReadout *dummyMask = pmReadoutAlloc(NULL, dummyImage, NULL, 0, 0, 1, 1);
+                    (void)pmFlatField(inputReadout, dummyMask, flatReadout);
+                    psFree(dummyMask);
+                    psFree(dummyImage);
+#endif
+                }
+
+                // Fringe subtraction
+                if (doFringe) {
+#if 0
+                    // Placeholder
+                    pmReadout *pmSubtractSky(pmReadout *in, psPolynomial2D *poly, psImage *mask,
+                                             psU8 maskVal, int binFactor, psStats *stats, float clipSD);
+#endif
+                }
+
+                // Source identification and photometry
+                if (doSource) {
+                }
+
+                // Astrometry
+                if (doAstrom) {
+                }
+
+
+            } // Iterating over readouts
+        } // Iterating over cells
+
+#if 0
+        {
+            // Testing pmChipMosaic
+            psImage *mosaic = pmChipMosaic(inputChip, 1, 1); // Mosaic of chip
+            psFits *mosaicFile = psFitsOpen("mosaic.fits", "w"); // FITS file to which to write
+            psFitsWriteImage(mosaicFile, NULL, mosaic, 0);
+            psFitsClose(mosaicFile);
+            psFree(mosaic);
+        }
+#endif
+
+    } // Iterating over chips
+
+    psLogMsg("phase2", PS_LOG_INFO, "Processing completed after %f sec.\n", psTimerMark("phase2"));
+
+
+#if 0
+    {
+        // Testing pmFPAMorph
+        int nBad = 0;
+        psMetadata *newCamera = psMetadataConfigParse(NULL, &nBad, "megacam_splice.config", true);
+        pmFPA *newFPA = pmFPAConstruct(newCamera);
+        pmFPAMorph(newFPA, input, true, 0, 0);
+        //pmFPAPrint(newFPA);
+        psFits *newFile = psFitsOpen("morph.fits", "w");
+        pmFPAWrite(newFile, newFPA, database);
+        psFitsClose(newFile);
+        psFree(newFPA);
+    }
+#endif
+
+#if 1
+    // Write the output
+    pmFPAWrite(outputFile, input, database);
+    pmFPAWriteMask(input, outputFile);
+    pmFPAWriteWeight(input, outputFile);
+#endif
+
+    psLogMsg("phase2", PS_LOG_INFO, "Output completed after %f sec.\n", psTimerMark("phase2"));
+
+    psFitsClose(outputFile);
+
+    psFree(arguments);
+    psFree(site);
+    psFree(header);
+    psFree(camera);
+    psFree(recipe);
+    psFree(input);
+    psFree(mask);
+    psFree(bias);
+    psFree(dark);
+    psFree(flat);
+    psFree(overscanFit);
+    psFree(overscanStats);
+
+    psLogMsg("phase2", PS_LOG_INFO, "Cleanup completed after %f sec.\n", psTimerMark("phase2"));
+    psTimerStop();
+
+#if 0
+    psMemCheckCorruption(true);
+    psMemBlock **leaks = NULL;          // List of leaks
+    int nLeaks = psMemCheckLeaks(0, &leaks, NULL, false); // Number of leaks
+    printf("%d leaks found.\n", nLeaks);
+#if 1
+    for (int i = 0; i < nLeaks; i++) {
+        psMemBlock *mb = leaks[i];
+        printf("Memory leak %ld (%ld):\n"
+               "\tFile %s, line %d, size %d\n"
+               "\tPosts: %x %x %x\n",
+               mb->id, mb->refCounter, mb->file, mb->lineno, mb->userMemorySize, mb->startblock, mb->endblock,
+               *(void**)((int8_t *)(mb + 1) + mb->userMemorySize));
+    }
+#endif
+#endif
+
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.c	(revision 7462)
@@ -0,0 +1,56 @@
+#include <stdio.h>
+#include <strings.h>
+
+#include "pslib.h"
+#include "psAdditionals.h"
+
+#include "papStuff.h"
+
+
+static void memPrint(const psPtr ptr)
+{
+    psMemBlock *mb = ((psMemBlock*)ptr) - 1;
+    printf("Memory block %lld (%lld):\n"
+	   "\tFile %s, line %d, size %d\n"
+	   "\tPosts: %x %x %x\n",
+	   mb->id, mb->refCounter, mb->file, mb->lineno, mb->userMemorySize, mb->startblock, mb->endblock,
+	   *(void**)((int8_t *)(mb + 1) + mb->userMemorySize));
+}
+
+// Split string on given characters
+psList *papSplit(const char *string, const char *splitters)
+{
+    psList *values = psListAlloc(NULL);	// The list of values to return
+    unsigned int length = strlen(string); // The length of the string
+    unsigned int numSplitters = strlen(splitters); // Number of characters that might split
+    unsigned int start = 0;		// The position of the start of a word
+    for (int i = 1; i < length; i++) {
+	bool split = false;		// Is this character a splitter?
+	for (int j = 0; j < numSplitters && ! split; j++) {
+	    if (string[i] == splitters[j]) {
+		split = true;
+	    }
+	}
+	if (split) {
+	    if (i == start) {
+		// Some idiot put in two spaces, or two commas or something
+		start++;
+	    } else {
+		// We're at the end of the word
+		psString word = psStringNCopy(&string[start], i - start);
+		(void)psListAdd(values, PS_LIST_TAIL, word);
+		start = i + 1;
+		psFree(word);
+	    }
+	}
+    }
+    if (start < length) {
+	// Copy the last word
+	psString word = psStringNCopy(&string[start], length - start);
+	(void)psListAdd(values, PS_LIST_TAIL, word);
+	psFree(word);
+    }
+
+    return values;
+}
+
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/papStuff.h	(revision 7462)
@@ -0,0 +1,9 @@
+#ifndef PAP_STUFF_H
+#define PAP_STUFF_H
+
+#include "pslib.h"
+
+// Split string on given characters
+psList *papSplit(const char *string, const char *splitters);
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/phase2.config
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/phase2.config	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/phase2.config	(revision 7462)
@@ -0,0 +1,65 @@
+### Phase 2 recipe configuration file
+
+# List of tasks to perform
+MASK		BOOL	FALSE		# Mask bad pixels
+NONLIN		BOOL	TRUE		# Non-linearity correction
+BIAS		BOOL	TRUE		# Bias subtraction
+DARK		BOOL	FALSE		# Dark subtraction
+OVERSCAN	BOOL	TRUE		# Overscan subtraction
+FLAT		BOOL	TRUE		# Flat-field normalisation
+FRINGE		BOOL	FALSE		# Fringe subtraction
+SOURCE		BOOL	FALSE		# Source identification and photometry
+ASTROM		BOOL	FALSE		# Astrometry
+
+
+# Non-linearity correction
+NONLIN.SOURCE		STR	CHIP.NAME	# How to determine the source
+#@NONLIN.DATA		F32	0.0 1.001 0.001	# A polynomial
+NONLIN.DATA		STR	nonlin.dat	# Filename for lookup table
+#NONLIN.DATA		METADATA		# Source of non-linearity data
+#	ccd00		STR	nonlin00.dat	# A lookup table 
+#	@ccd01		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd02		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd03		F32	1.2345 		# A polynomial
+#	@ccd04		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd05		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd06		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd07		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd08		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd09		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd10		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd11		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd12		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd13		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd14		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd15		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd16		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd17		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd18		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd19		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd10		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd21		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd22		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd23		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd24		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd25		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd26		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd27		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd28		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd29		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd30		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd31		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd32		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd33		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd34		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd35		F32	0.0 1.001 0.001	# A polynomial
+#	@ccd36		F32	0.0 1.001 0.001	# A polynomial
+#END
+
+# Overscan subtraction
+OVERSCAN.MODE		STR	INDIVIDUAL	# NONE | INDIVIDUAL | ALL
+#OVERSCAN.FIT		STR	SPLINE		# NONE | POLYNOMIAL | SPLINE
+OVERSCAN.FIT		STR	POLYNOMIAL	# NONE | POLYNOMIAL | SPLINE
+OVERSCAN.ORDER		S32	5		# Order of polynomial fit
+OVERSCAN.BIN		S32	0		# Number of pixels per bin
+OVERSCAN.STAT		STR	MEAN		# MEAN | MEDIAN
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.c	(revision 7462)
@@ -0,0 +1,175 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmChipMosaic.h"
+
+// Compare a value with a maximum and minimum
+#define COMPARE(value,min,max) \
+    if ((value) < (min)) { \
+        (min) = (value); \
+    } \
+    if ((value) > (max)) { \
+        (max) = (value); \
+    }
+
+// Mosaic multiple images, with flips, binning and offsets
+psImage *p_pmImageMosaic(const psArray *source, // Images to splice in
+                         const psVector *xFlip, const psVector *yFlip, // Need to flip x and y?
+                         const psVector *xBinSource, const psVector *yBinSource, // Binning in x and y of
+                                                                                 // source images
+                         int xBinTarget, int yBinTarget, // Binning in x and y of target images
+                         const psVector *x0, const psVector *y0 // Offsets for source images on target
+    )
+{
+    assert(source);
+    assert(xFlip && xFlip->type.type == PS_TYPE_U8);
+    assert(yFlip && yFlip->type.type == PS_TYPE_U8);
+    assert(xBinSource && xBinSource->type.type == PS_TYPE_S32);
+    assert(yBinSource && yBinSource->type.type == PS_TYPE_S32);
+    assert(x0 && x0->type.type == PS_TYPE_S32);
+    assert(y0 && y0->type.type == PS_TYPE_S32);
+
+    // Get the maximum extent of the mosaic image
+    int xMin = INT_MAX;
+    int xMax = 0;
+    int yMin = INT_MAX;
+    int yMax = 0;
+    for (int i = 0; i < source->n; i++) {
+        psImage *image = source->data[i]; // The image of interest
+
+        assert(image->type.type == PS_TYPE_F32); // Only implemented for F32 images so far.
+
+        // Size of cell in x and y
+        int xParity = xFlip->data.U8[i] ? -1 : 1;
+        int yParity = yFlip->data.U8[i] ? -1 : 1;
+        psTrace(__func__, 5, "Extent of cell %d: %d -> %d , %d -> %d\n", i, x0->data.S32[i],
+                x0->data.S32[i] + xParity * xBinSource->data.S32[i] * image->numCols, y0->data.S32[i],
+                y0->data.S32[i] + yParity * yBinSource->data.S32[i] * image->numRows);
+
+        COMPARE(x0->data.S32[i], xMin, xMax);
+        COMPARE(y0->data.S32[i], yMin, yMax);
+        // Subtract the parity to get the inclusive limit (not exclusive)
+        COMPARE(x0->data.S32[i] + xParity * xBinSource->data.S32[i] * image->numCols - xParity, xMin, xMax);
+        COMPARE(y0->data.S32[i] + yParity * yBinSource->data.S32[i] * image->numRows - yParity, yMin, yMax);
+    }
+
+    // Set up the image
+    // Since both upper and lower values are inclusive, we need to add one to the size
+    float xSize = (float)(xMax - xMin + 1) / (float)xBinTarget;
+    if (xSize - (int)xSize > 0) {
+        xSize += 1;
+    }
+    float ySize = (float)(yMax - yMin + 1) / (float)yBinTarget;
+    if (ySize - (int)ySize > 0) {
+        ySize += 1;
+    }
+
+    psTrace(__func__, 3, "Spliced image will be %dx%d\n", (int)xSize, (int)ySize);
+    psImage *mosaic = psImageAlloc((int)xSize, (int)ySize, PS_TYPE_F32); // The mosaic image
+    psImageInit(mosaic, 0.0);
+
+    // Next pass through the images to do the mosaicking
+    for (int i = 0; i < source->n; i++) {
+        psImage *image = source->data[i]; // The image of interest
+        if (xBinSource->data.S32[i] == xBinTarget && yBinSource->data.S32[i] == yBinTarget &&
+            xFlip->data.U8[i] == 0 && yFlip->data.U8[i] == 0) {
+            // Let someone else do the hard work; useful to test psImageOverlaySection if no other reason
+            psImageOverlaySection(mosaic, image, x0->data.S32[i], y0->data.S32[i], "+");
+        } else {
+            // We have to do the hard work ourself
+            for (int y = 0; y < image->numRows; y++) {
+                int yParity = yFlip->data.U8[i] ? -1 : 1;
+                float yTargetBase = (y0->data.S32[i] + yParity * yBinSource->data.S32[i] * y) / yBinTarget;
+                for (int x = 0; x < image->numCols; x++) {
+                    int xParity = xFlip->data.U8[i] ? -1 : 1;
+                    float xTargetBase = (x0->data.S32[i] + xParity * xBinSource->data.S32[i] * x) /
+                        xBinTarget;
+
+                    // In case the original image is binned but the mosaic is not, we need to fill in the
+                    // values in the mosaic.
+                    for (int j = 0; j < yBinSource->data.S32[i]; j++) {
+                        int yTarget = (int)(yTargetBase + yParity * (float)j / (float)yBinTarget);
+                        for (int i = 0; i < xBinSource->data.S32[i]; i++) {
+                            int xTarget = (int)(xTargetBase + xParity * (float)i / (float)xBinTarget);
+
+                            mosaic->data.F32[yTarget][xTarget] += image->data.F32[y][x];
+                        }
+                    } // Iterating over mosaic image for binned input image
+                }
+            } // Iterating over input image
+        }
+    }
+
+    return mosaic;
+}
+
+psImage *pmChipMosaic(pmChip *chip,     // Chip to mosaic
+                      int xBinChip, int yBinChip // Binning of mosaic image in x and y
+    )
+{
+    psArray *cells = chip->cells;       // The array of cells
+    psArray *images = psArrayAlloc(cells->n); // Array of images that will be mosaicked
+    psVector *x0 = psVectorAlloc(cells->n, PS_TYPE_S32); // Origin x coordinates
+    psVector *y0 = psVectorAlloc(cells->n, PS_TYPE_S32); // Origin y coordinates
+    psVector *xBin = psVectorAlloc(cells->n, PS_TYPE_S32); // Binning in x
+    psVector *yBin = psVectorAlloc(cells->n, PS_TYPE_S32); // Binning in y
+    psVector *xFlip = psVectorAlloc(cells->n, PS_TYPE_U8); // Flip in x?
+    psVector *yFlip = psVectorAlloc(cells->n, PS_TYPE_U8); // Flip in y?
+
+    // Set up the required inputs
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        x0->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+        y0->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+        xBin->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN");
+        yBin->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.XBIN");
+        int xParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.XPARITY");
+        int yParity = psMetadataLookupS32(NULL, cell->concepts, "CELL.YPARITY");
+        if (xParity == 1) {
+            xFlip->data.U8[i] = 0;
+        } else if (xParity == -1) {
+            xFlip->data.U8[i] = 1;
+        } else {
+            psLogMsg(__func__, PS_LOG_WARN, "The x parity of cell %d is not +/- 1 (it's %d) --- "
+                     "assuming +1.\n", i, xParity);
+            xFlip->data.U8[i] = 0;
+        }
+        if (yParity == 1) {
+            yFlip->data.U8[i] = 0;
+        } else if (yParity == -1) {
+            yFlip->data.U8[i] = 1;
+        } else {
+            psLogMsg(__func__, PS_LOG_WARN, "The y parity of cell %d is not +/- 1 (it's %d) --- "
+                     "assuming +1.\n", i, yParity);
+            yFlip->data.U8[i] = 0;
+        }
+
+        // Trim the image to get rid of the overscan
+        psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC");
+        psTrace(__func__, 7, "Cell %d trimsec: [%.0f:%.0f,%.0f:%.0f]\n", i, trimsec->x0, trimsec->x1,
+                trimsec->y0, trimsec->y1);
+        psArray *readouts = cell->readouts; // The array of readouts
+        if (readouts->n > 1) {
+            psLogMsg(__func__, PS_LOG_WARN, "Cell %d contains more than one readout --- only the first will "
+                     "be mosaicked.\n", i);
+        }
+        psImage *image = ((pmReadout*)readouts->data[0])->image; // The image to put into the mosaic
+        images->data[i] = psImageSubset(image, *trimsec); // Trimmed image
+    }
+
+    // Mosaic the images together and we're done
+    psImage *mosaic = p_pmImageMosaic(images, xFlip, yFlip, xBin, yBin, xBinChip, yBinChip, x0, y0);
+
+    // Clean up
+    psFree(x0);
+    psFree(y0);
+    psFree(xBin);
+    psFree(yBin);
+    psFree(xFlip);
+    psFree(yFlip);
+    psFree(images);
+
+    return mosaic;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmChipMosaic.h	(revision 7462)
@@ -0,0 +1,22 @@
+#ifndef PM_CHIP_MOSAIC_H
+#define PM_CHIP_MOSAIC_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+// Mosaic multiple images, with flips, binning and offsets
+psImage *p_pmImageMosaic(const psArray *source, // Images to splice in
+                         const psVector *xFlip, const psVector *yFlip, // Need to flip x and y?
+                         const psVector *xBinSource, const psVector *yBinSource, // Binning in x and y of
+                                                                                 // source images
+                         int xBinTarget, int yBinTarget, // Binning in x and y of target images
+                         const psVector *x0, const psVector *y0 // Offsets for source images on target
+    );
+
+// Mosaic all the cells in a chip together (neglecting the overscans)
+psImage *pmChipMosaic(pmChip *chip,     // Chip to mosaic
+                      int xBinChip, int yBinChip // Binning of mosaic image in x and y
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.c	(revision 7462)
@@ -0,0 +1,308 @@
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include "pslib.h"
+#include "psAdditionals.h"
+#include "pmConfig.h"
+
+#define PS_SITE "PS_SITE"		// Name of the environment variable containing the site config file
+#define DEFAULT_SITE "ipprc.config"	// Default site config file
+
+static bool readConfig(psMetadata **config, // Config to output
+		       const char *name,// Name of file
+		       const char *description // Description of file
+    )
+{
+    int numBadLines = 0;		// Number of bad lines in config file
+    psLogMsg(__func__, PS_LOG_INFO, "Loading %s configuration from file %s\n", description, name);
+    *config = psMetadataConfigParse(NULL, &numBadLines, name, true);
+    if (numBadLines > 0) {
+	psLogMsg(__func__, PS_LOG_WARN, "%d bad lines in %s configuration file (%s)\n", description,
+		 name);
+    }
+    if (! *config) {
+	psError(PS_ERR_IO, false, "Unable to read %s configuration from %s\n", description, name);
+	return false;
+    }
+
+    return true;
+}
+
+
+bool pmConfigRead(psMetadata **site, psMetadata **camera, psMetadata **recipe,
+                  int *argc, char **argv, const char *recipeName)
+{
+    // Make sure we've been given the correct inputs, and that we won't leak memory
+    assert(site && *site == NULL);
+    assert(camera && *camera == NULL);
+    assert(recipe && *recipe == NULL);
+    assert(*argc > 0);
+    assert(argv);
+
+    char *siteName = NULL;		// Name of the site configuration file
+
+    // First, try command line
+    int argNum = 0;			// Number of the site argument
+    if (argNum = psArgumentGet(*argc, argv, "-site")) {
+	(void)psArgumentRemove(argNum, argc, argv);
+	if (argNum >= *argc) {
+	    psLogMsg(__func__, PS_LOG_WARN,
+		     "-site command-line switch provided without the required filename --- ignored.\n");
+	} else {
+	    siteName = argv[argNum];
+	    (void)psArgumentRemove(argNum, argc, argv);
+	}
+    }
+    // Next, try environment variable
+    if (! siteName) {
+	siteName = getenv(PS_SITE);
+    }
+    // Last chance is ~/.ipprc
+    bool cleanupSiteName = false;	// Do I have to psFree siteName?
+    if (! siteName) {
+	siteName = psStringCopy(DEFAULT_SITE);
+	cleanupSiteName = true;
+    }
+
+    if (! readConfig(site, siteName, "site")) {
+	if (cleanupSiteName) {
+	    psFree(siteName);
+	}
+	return false;
+    }
+
+    // Next is the camera configuration
+    if (argNum = psArgumentGet(*argc, argv, "-camera")) {
+	(void)psArgumentRemove(argNum, argc, argv);
+	if (argNum >= *argc) {
+	    psLogMsg(__func__, PS_LOG_WARN,
+		     "-camera command-line switch provided without the required filename --- ignored.\n");
+	} else {
+	    (void)psArgumentRemove(argNum, argc, argv);
+	    (void)readConfig(camera, argv[argNum], "camera");
+	}
+    }
+
+    // And then the recipe configuration
+    if (argNum = psArgumentGet(*argc, argv, "-recipe")) {
+	(void)psArgumentRemove(argNum, argc, argv);
+	if (argNum >= *argc) {
+	    psLogMsg(__func__, PS_LOG_WARN,
+		     "-recipe command-line switch provided without the required filename --- ignored.\n");
+	} else {
+	    (void)psArgumentRemove(argNum, argc, argv);
+	    (void)readConfig(recipe, argv[argNum], "recipe");
+	}
+    }
+    // Or, load the recipe from the camera file, if appropriate
+    if (! *recipe && *camera && recipeName) {
+	*recipe = pmConfigRecipeFromCamera(*camera, recipeName);
+    }
+
+
+    // Now we can look into the site configuration and do the required stuff
+    bool mdok = true;			// Status of MD lookup result
+    psString timeName = psMetadataLookupString(&mdok, *site, "TIME"); // Name of time file
+    if (mdok && timeName) {
+	psTrace(__func__, 7, "Initialising psTime with file %s\n", timeName);
+#ifdef PRODUCTION
+	psTimeInitialize(timeName);
+#else
+	psLibInit(timeName);
+#endif
+    }
+
+    int logLevel = psMetadataLookupS32(&mdok, *site, "LOGLEVEL"); // Logging level
+    if (mdok && logLevel >= 0) {
+	psTrace(__func__, 7, "Setting log level to %d\n", logLevel);
+	psLogSetLevel(logLevel);
+    }
+
+    psString logFormat = psMetadataLookupString(&mdok, *site, "LOGLEVEL"); // Log format
+    if (mdok && logFormat) {
+	psTrace(__func__, 7, "Setting log format to %s\n", logFormat);
+	psLogSetFormat(logFormat);
+    }
+
+    psString logDest = psMetadataLookupString(&mdok, *site, "LOGDEST"); // Log destination
+    if (mdok && logDest) {
+	// XXX: Only stdout is provided for now; this section should be expanded in the future to do files,
+	// and perhaps even sockets.
+	if (strcasecmp(logDest, "STDOUT") != 0) {
+	    psLogMsg(__func__, PS_LOG_WARN, "Only STDOUT is currently supported as a log destination.\n");
+	}
+	psTrace(__func__, 7, "Setting log destination to STDOUT.\n");
+	psLogSetDestination(PS_LOG_TO_STDOUT);
+    }
+
+    psMetadata *trace = psMetadataLookupMD(&mdok, *site, "TRACE"); // Trace levels
+    if (mdok && trace) {
+	psMetadataIterator *traceIter = psMetadataIteratorAlloc(trace, PS_LIST_HEAD, NULL); // Iterator
+	psMetadataItem *traceItem = NULL; // Item from MD iteration
+	while (traceItem = psMetadataGetAndIncrement(traceIter)) {
+	    if (traceItem->type != PS_DATA_S32) {
+		psLogMsg(__func__, PS_LOG_WARN, "The level for trace component %s is not of type S32 (%x)\n",
+			 traceItem->name, traceItem->type);
+		continue;
+	    }
+	    psTrace(__func__, 7, "Setting trace level for %s to %d\n", traceItem->name, traceItem->data.S32);
+	    psTraceSetLevel(traceItem->name, traceItem->data.S32);
+	}
+	psFree(traceIter);
+    }
+
+    if (cleanupSiteName) {
+	psFree(siteName);
+    }
+    return true;
+}
+
+bool pmConfigValidateCamera(const psMetadata *camera, const psMetadata *header)
+{
+    // Read the rule for that camera
+    bool mdStatus = true;		// Status of MD lookup
+    psMetadata *rule = psMetadataLookupMD(&mdStatus, camera, "RULE");
+    if (! mdStatus || ! rule) {
+	psLogMsg(__func__, PS_LOG_WARN, "Unable to read rule for camera.\n");
+	return false;
+    }
+
+    // Apply the rules
+    psMetadataIterator *ruleIter = psMetadataIteratorAlloc(rule, PS_LIST_HEAD, NULL); // Rule iterator
+    psMetadataItem *ruleItem = NULL;	// Item from the metadata
+    bool match = true;			// Does it match?
+    while ((ruleItem = psMetadataGetAndIncrement(ruleIter)) && match) {
+	// Check for the existence of the rule
+	psMetadataItem *headerItem = psMetadataLookup((psMetadata*)header, ruleItem->name);
+	if (! headerItem || headerItem->type != ruleItem->type) {
+	    match = false;
+	    break;
+	}
+
+	// Check to see if the rule works
+	switch (ruleItem->type) {
+	  case PS_DATA_STRING:
+	    psTrace(__func__, 8, "Matching %s: '%s' vs '%s'\n", ruleItem->name,
+		    ruleItem->data.V, headerItem->data.V);
+	    if (strncmp(ruleItem->data.V, headerItem->data.V,
+			    strlen(ruleItem->data.V)) != 0) {
+		match = false;
+	    }
+	    break;
+	  case PS_TYPE_S32:
+	  case PS_TYPE_BOOL:
+	    psTrace(__func__, 8, "Matching %s: %d vs %d\n", ruleItem->name,
+		    ruleItem->data.S32, headerItem->data.S32);
+	    if (ruleItem->data.S32 != headerItem->data.S32) {
+		match = false;
+	    }
+	    break;
+	  case PS_TYPE_F32:
+	    psTrace(__func__, 8, "Matching %s: %f vs %f\n", ruleItem->name,
+		    ruleItem->data.F32, headerItem->data.F32);
+	    if (ruleItem->data.F32 != headerItem->data.F32) {
+		match = false;
+	    }
+	    break;
+	  case PS_TYPE_F64:
+	    psTrace(__func__, 8, "Matching %s: %g vs %g\n", ruleItem->name,
+		    ruleItem->data.F64, headerItem->data.F64);
+	    if (ruleItem->data.F64 != headerItem->data.F64) {
+		match = false;
+	    }
+	    break;
+	  default:
+	    psLogMsg(__func__, PS_LOG_WARN, "Ignoring invalid type in metadata: %x\n",
+		     ruleItem->type);
+	}
+    } // Iterating through the RULEs
+
+    psFree(ruleIter);
+
+    return match;
+}
+    
+
+
+// Work out what camera we have, based on the FITS header and a set of rules specified in the IPP
+// configuration; return the camera configuration
+psMetadata *pmConfigCameraFromHeader(const psMetadata *ipprc, // The IPP configuration
+				     const psMetadata *header // The FITS header
+    )
+{
+    bool mdStatus = false;		// Metadata lookup status
+    psMetadata *cameras = psMetadataLookupMD(&mdStatus, ipprc, "CAMERAS");
+    if (! mdStatus) {
+	psError(PS_ERR_IO, false, "Unable to find CAMERAS in the configuration.\n");
+	return NULL;
+    }
+
+    psMetadata *winner = NULL;	      // The camera configuration whose rule first matches the supplied header
+
+    // Iterate over the cameras
+    psMetadataIterator *iterator = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // MD Iterator
+    psMetadataItem *cameraItem = NULL; // Item from the metadata
+    while (cameraItem = psMetadataGetAndIncrement(iterator)) {
+	// Open the camera information
+	psTrace(__func__, 3, "Inspecting camera %s (%s)\n", cameraItem->name,
+		cameraItem->comment);
+	psMetadata *camera = NULL;	// The camera metadata
+	if (cameraItem->type == PS_DATA_METADATA) {
+	    camera = psMemIncrRefCounter(cameraItem->data.md);
+	} else if (cameraItem->type == PS_DATA_STRING) {
+	    psTrace(__func__, 5, "Reading camera configuration for %s...\n", cameraItem->name);
+	    int badLines = 0;		// Number of bad lines in reading camera configuration
+	    camera = psMetadataConfigParse(NULL, &badLines, cameraItem->data.V, true);
+	    if (badLines > 0) {
+		psLogMsg(__func__, PS_LOG_WARN, "%d bad lines encountered while reading camera"
+			 "configuration %s\n", badLines, cameraItem->name);
+	    }
+	}
+
+	if (! camera) {
+	    psLogMsg(__func__, PS_LOG_WARN, "Unable to interpret camera configuration for %s (%s)\n",
+		     cameraItem->name, cameraItem->comment);
+	    continue;
+	}
+
+	if (pmConfigValidateCamera(camera, header)) {
+	    if (! winner) {
+		// This is the first match
+		winner = psMemIncrRefCounter(camera);
+		psLogMsg(__func__, PS_LOG_INFO, "FITS header matches camera %s\n",
+			 cameraItem->name);
+	    } else {
+		// We have a duplicate match
+		psLogMsg(__func__, PS_LOG_WARN, "Additional camera found that matches the rules: %s\n",
+			 cameraItem->name);
+	    }
+	} // Done inspecting the camera
+
+	psFree(camera);
+	
+    } // Done looking at all cameras
+    if (! winner) {
+	psError(PS_ERR_IO, true, "Unable to find a camera that matches input FITS header!\n");
+    }
+
+    psFree(iterator);
+    return winner;
+}
+
+psMetadata *pmConfigRecipeFromCamera(const psMetadata *camera, const char *recipeName)
+{
+    assert(camera);
+    assert(recipeName);
+
+    psMetadata *recipe = NULL;	// Recipe to read
+    bool mdok = true;			// Status of MD lookup
+    psMetadata *recipes = psMetadataLookupMD(&mdok, camera, "RECIPES"); // The list of recipes
+    if (! mdok || ! recipes) {
+	psLogMsg(__func__, PS_LOG_WARN, "RECIPES in the camera configuration file is not of type METADATA\n");
+    } else {
+	psString recipeFileName = psMetadataLookupString(&mdok, recipes, recipeName);
+	(void)readConfig(&recipe, recipeFileName, "recipe");
+    }
+
+    return recipe;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmConfig.h	(revision 7462)
@@ -0,0 +1,12 @@
+#ifndef PM_CONFIG_H
+#define PM_CONFIG_H
+
+#include "pslib.h"
+
+bool pmConfigRead(psMetadata **site, psMetadata **camera, psMetadata **recipe,
+                  int *argc, char **argv, const char *recipeName);
+bool pmConfigValidateCamera(const psMetadata *camera, const psMetadata *header);
+psMetadata *pmConfigCameraFromHeader(const psMetadata *site, const psMetadata *header);
+psMetadata *pmConfigRecipeFromCamera(const psMetadata *camera, const char *recipeName);
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.c	(revision 7462)
@@ -0,0 +1,293 @@
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include "pslib.h"
+#include "psAdditionals.h"
+
+#include "pmFPA.h"
+
+// Get the bias images for a readout, using the CELL.BIASSEC
+psList *pmReadoutGetBias(pmReadout *readout // Readout for which to get the bias sections
+    )
+{
+    pmCell *cell = readout->parent;	// The parent cell
+    psList *sections = (psList*)psMetadataLookupPtr(NULL, cell->concepts, "CELL.BIASSEC"); // CELL.BIASSEC
+
+    psImage *image = readout->image;	// The image that contains both the biassec and the trimsec
+
+    psList *images = psListAlloc(NULL);	// List of images from bias sections
+    psListIterator *sectionsIter = psListIteratorAlloc(sections, PS_LIST_HEAD, true); // Iterator
+    psRegion *region = NULL;		// Bias region from list
+    while (region = psListGetAndIncrement(sectionsIter)) {
+#if 0
+	// Convert from FITS standard to PS standard
+	// We don't touch the x1 and y1 values because they aren't included by psImageSubset.
+	region->x0 -= 1;
+	region->y0 -= 1;
+#endif
+
+	psImage *bias = psImageSubset(image, *region); // Image from bias section
+	psListAdd(images, PS_LIST_TAIL, bias);
+	psFree(bias);
+    }
+    psFree(sectionsIter);
+
+    return images;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Allocators
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+p_pmHDU *p_pmHDUAlloc(const char *extname)
+{
+    p_pmHDU *pd = psAlloc(sizeof(p_pmHDU));
+    psMemSetDeallocator(pd, (psFreeFunc)p_pmHDUFree);
+
+    pd->extname = extname;
+    pd->header = NULL;
+    pd->images = NULL;
+    pd->masks = NULL;
+    pd->weights = NULL;
+
+    return pd;
+}
+
+void p_pmHDUFree(p_pmHDU *hdu)
+{
+    psFree(hdu->header);
+    psFree(hdu->images);
+    psFree(hdu->masks);
+    psFree(hdu->weights);
+}
+
+pmFPA *pmFPAAlloc(const psMetadata *camera // Camera configuration
+    )
+{
+    pmFPA *fpa = psAlloc(sizeof(pmFPA));// The FPA
+    psMemSetDeallocator(fpa, (psFreeFunc)p_pmFPAFree);
+
+    // Fill in the components
+    fpa->fromTangentPlane = NULL;
+    fpa->toTangentPlane = NULL;
+    fpa->projection = NULL;
+
+    fpa->concepts = psMetadataAlloc();
+    fpa->camera = psMemIncrRefCounter((psPtr)camera);
+    fpa->chips = psArrayAlloc(0);
+
+    fpa->phu = NULL;
+    fpa->hdu = NULL;
+
+    return fpa;
+}
+
+void p_pmFPAFree(pmFPA *fpa)
+{
+    psFree(fpa->fromTangentPlane);
+    psFree(fpa->toTangentPlane);
+    psFree(fpa->projection);
+
+    psFree(fpa->concepts);
+    psFree(fpa->camera);
+    psFree(fpa->chips);
+
+    psFree(fpa->phu);
+    psFree(fpa->hdu);
+}
+
+pmChip *pmChipAlloc(pmFPA *fpa,	// FPA to which the chip belongs
+		    psString name	// Chip name
+    )
+{
+    pmChip *chip = psAlloc(sizeof(pmChip)); // The chip
+    psMemSetDeallocator(chip, (psFreeFunc)p_pmChipFree);
+
+    // Push onto the array of chips
+    fpa->chips = psArrayAdd(fpa->chips, 0, chip);
+
+    // Fill in the components
+    *(int*)&chip->col0 = 0;		// Good enough for now
+    *(int*)&chip->row0 = 0;		// Good enough for now
+
+    chip->toFPA = NULL;
+    chip->fromFPA = NULL;
+
+    chip->concepts = psMetadataAlloc();
+    chip->cells = psArrayAlloc(0);
+    chip->parent = fpa;			// We don't increment the reference counter on this --- it's a
+					// "hidden" link.  If we increment the reference counter, we get stuck
+					// in a circle.
+    chip->valid = true;    
+
+    chip->hdu = NULL;
+
+    psMetadataAddStr(chip->concepts, PS_LIST_HEAD, "CHIP.NAME", 0, "Chip name added at pmChipAlloc", name);
+
+    return chip;
+}
+
+void p_pmChipFree(pmChip *chip)
+{
+    psFree(chip->toFPA);
+    psFree(chip->fromFPA);
+
+    psFree(chip->concepts);
+    psFree(chip->cells);
+
+    psFree(chip->hdu);
+
+    // We don't free the parent member, since that would generate a circular call.  We don't increment the
+    // reference counter when we add it, anyway, so that's OK.
+}
+
+pmCell *pmCellAlloc(pmChip *chip,	// Chip to which the cell belongs
+		    psMetadata *cameraData, // Camera data
+		    psString name	// Name of cell
+    )
+{
+    pmCell *cell = psAlloc(sizeof(pmCell)); // The cell
+    psMemSetDeallocator(cell, (psFreeFunc)p_pmCellFree);
+
+    // Push onto the array of chips
+    chip->cells = psArrayAdd(chip->cells, 0, cell);
+
+    // Fill in components
+    *(int*)&cell->col0 = 0;		// Good enough for now
+    *(int*)&cell->row0 = 0;		// Good enough for now
+
+    cell->toChip = NULL;
+    cell->toFPA = NULL;
+    cell->toSky = NULL;
+
+    cell->concepts = psMetadataAlloc();
+    psMetadataAddStr(cell->concepts, PS_LIST_HEAD, "CELL.NAME", 0, "Cell name added at pmCellAlloc", name);
+    cell->camera = psMemIncrRefCounter(cameraData);
+
+    cell->readouts = psArrayAlloc(0);
+    cell->parent = chip;		// We don't increment the reference counter on this --- it's a
+					// "hidden" link.  If we increment the reference counter, we get stuck
+					// in a circle.
+    cell->valid = true;
+
+    cell->hdu = NULL;
+
+    return cell;
+}
+
+void p_pmCellFree(pmCell *cell)
+{
+    psFree(cell->toChip);
+    psFree(cell->toFPA);
+    psFree(cell->toSky);
+
+    psFree(cell->concepts);
+    psFree(cell->camera);
+    psFree(cell->readouts);
+
+    psFree(cell->hdu);
+
+    // We don't free the parent member, since that would generate a circular call.  We don't increment the
+    // reference counter when we add it, anyway, so that's OK.
+}
+
+pmReadout *pmReadoutAlloc(pmCell *cell, // Cell to which the readout belongs
+			  psImage *image, // The pixels
+			  psImage *mask,// The mask pixels
+			  int col0, int row0, int colBin, int rowBin // Data
+    )
+{
+    pmReadout *readout = psAlloc(sizeof(pmReadout));
+    psMemSetDeallocator(readout, (psFreeFunc)p_pmReadoutFree);
+    if (cell) {
+	cell->readouts = psArrayAdd(cell->readouts, 0, readout);
+    }
+    
+    // Set the components
+    readout->image = psMemIncrRefCounter(image);
+    readout->mask = psMemIncrRefCounter(mask);
+    readout->weight = NULL;
+    
+    //readout->concepts = psMetadataAlloc();
+    
+    *(int*)&readout->col0 = col0;
+    *(int*)&readout->row0 = row0;
+    *(int*)&readout->colBins = colBin;
+    *(int*)&readout->rowBins = rowBin;
+
+    readout->parent = cell;
+
+    return readout;
+}
+
+void p_pmReadoutFree(pmReadout *readout)
+{
+    psFree(readout->image);
+    psFree(readout->mask);
+    psFree(readout->weight);
+    //psFree(readout->concepts);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Select and Exclude chips
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmFPASelectChip(pmFPA *fpa, int chipNum)
+{
+    assert(fpa);
+
+    if (chipNum < 0 || chipNum > fpa->chips->n) {
+	return false;
+    }
+    psArray *chips = fpa->chips;	// Component chips
+    for (int i = 0; i < chips->n; i++) {
+	pmChip *chip = chips->data[i];	// The chip of interest
+	if (i == chipNum) {
+	    psTrace(__func__, 5, "Marking chip %d valid.\n", i);
+	    chip->valid = true;
+	    psArray *cells = chip->cells; // Component cells
+	    for (int j = 0; j < cells->n; j++) {
+		pmCell *cell = cells->data[j]; // Cell of interest
+		cell->valid = true;
+	    }
+	} else {
+	    psTrace(__func__, 5, "Marking chip %d invalid.\n", i);
+	    chip->valid = false;
+	    psArray *cells = chip->cells; // Component cells
+	    for (int j = 0; j < cells->n; j++) {
+		pmCell *cell = cells->data[j]; // Cell of interest
+		cell->valid = false;
+	    }
+	}
+    }
+    return true;
+}
+
+int pmFPAExcludeChip(pmFPA *fpa, int chipNum)
+{
+    assert(fpa);
+
+    if (chipNum < 0 || chipNum > fpa->chips->n) {
+	psLogMsg(__func__, PS_LOG_WARN, "Invalid chip number: %d\n", chipNum);
+    }
+    int numValid = 0;			// Number of valid chips
+    psArray *chips = fpa->chips;	// Component chips
+    for (int i = 0; i < chips->n; i++) {
+	pmChip *chip = chips->data[i];	// The chip of interest
+	if (i == chipNum) {
+	    psTrace(__func__, 5, "Marking chip %d invalid.\n", i);
+	    chip->valid = false;
+	    psArray *cells = chip->cells; // Component cells
+	    for (int j = 0; j < cells->n; j++) {
+		pmCell *cell = cells->data[j]; // Cell of interest
+		cell->valid = false;
+	    }
+	} else if (chip->valid) {
+	    numValid++;
+	}
+    }
+
+    psTrace(__func__, 5, "%d valid chips in fpa.\n", numValid);
+    return numValid;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPA.h	(revision 7462)
@@ -0,0 +1,135 @@
+#ifndef PM_FPA_H
+#define PM_FPA_H
+
+#include "pslib.h"
+#include "psAdditionals.h"
+
+// Temporary metadata types
+//#define PS_META_CHIP PS_DATA_UNKNOWN
+//#define PS_META_CELL PS_DATA_UNKNOWN
+
+typedef struct {
+    const char *extname;		// Extension name, if it corresponds to this level
+    psMetadata *header;			// The FITS header, if it corresponds to this level
+    psArray *images;			// The pixel data, if it corresponds to this level
+    psArray *masks;			// The mask data, if it corresponds to this level
+    psArray *weights;			// The weight data, if it corresponds to this level
+} p_pmHDU;
+
+typedef struct {
+    // Astrometric transformations
+    psPlaneDistort* fromTangentPlane;	// Transformation from tangent plane to focal plane
+    psPlaneDistort* toTangentPlane;	// Transformation from focal plane to tangent plane
+    psProjection *projection;		// Projection from tangent plane to sky
+    // Information
+    psMetadata *concepts;		// Cache for PS concepts
+    psMetadata *analysis;		// FPA-level analysis metadata
+    const psMetadata *camera;		// Camera configuration
+    psArray *chips;			// The chips
+    p_pmHDU *hdu;			// FITS data
+    psMetadata *phu;			// Primary Header
+} pmFPA;
+
+typedef struct {
+    // Offset specifying position on focal plane
+    int col0;				// Offset from the left of FPA.
+    int row0;				// Offset from the bottom of FPA.
+    // Astrometric transformations
+    psPlaneTransform* toFPA;		// Transformation from chip to FPA coordinates
+    psPlaneTransform* fromFPA;		// Transformation from FPA to chip coordinates
+    // Information
+    psMetadata *concepts;		// Cache for PS concepts
+    psMetadata *analysis;		// Chip-level analysis metadata
+    psArray *cells;			// The cells (referred to by name)
+    pmFPA *parent;			// Parent FPA
+    bool valid;				// Do we bother about reading and working with this chip?
+    p_pmHDU *hdu;			// FITS data
+} pmChip;
+
+typedef struct {
+    // Offset specifying position on chip
+    int col0;				// Offset from the left of chip.
+    int row0;				// Offset from the bottom of chip.
+    // Astrometric transformations
+    psPlaneTransform* toChip;		// Transformations from cell to chip coordinates
+    psPlaneTransform* toFPA;		// Transformations from cell to FPA coordinates
+    psPlaneTransform* toSky;		// Transformations from cell to sky coordinates
+    // Information
+    psMetadata *concepts;		// Cache for PS concepts
+    psMetadata *camera;			// Camera information
+    psMetadata *analysis;		// Cell-level analysis metadata
+    psArray *readouts;			// The readouts (referred to by number)
+    pmChip *parent;			// Parent chip
+    bool valid;				// Do we bother about reading and working with this cell?
+    p_pmHDU *hdu;			// FITS data
+} pmCell;
+
+typedef struct {
+    // Position on the cell
+    int col0;				// Offset from the left of cell.
+    int row0;				// Offset from the bottom of cell.
+    int colBins;			// Amount of binning in x-dimension and parity (from sign)
+    int rowBins;			// Amount of binning in y-dimension and parity (from sign)
+    // Information
+    psImage *image;			// Imaging area of readout
+    psImage *mask;			// Mask for image
+    psImage *weight;			// Weight for image
+#if 0
+    psList *bias;			// List of bias section (sub-)images
+#endif
+    psMetadata *analysis;		// Readout-level analysis metadata
+    pmCell *parent;			// Parent cell
+} pmReadout;
+
+#if 0
+typedef struct {
+    // Details for position on the cell
+    const int col0;			// Offset from the left of cell.
+    const int row0;			// Offset from the bottom of cell.
+    const int colParity;		// Readout Direction X
+    const int rowParity;		// Readout Direction Y
+    const unsigned int colBins;		// Amount of binning in x-dimension
+    const unsigned int rowBins;		// Amount of binning in y-dimension
+    // Information
+    psMetadata *concepts;		// Concepts for readouts
+    pmCell *parent;			// Parent cell
+    psImage *image;			// The pixels
+    psImage *mask;			// Mask image
+    psImage *weight;			// Weight image
+} pmReadout;
+#endif
+
+psList *pmReadoutGetBias(pmReadout *readout // Readout for which to get the bias sections
+    );
+
+
+// Allocators and deallocators
+p_pmHDU *p_pmHDUAlloc(const char *extname);
+void p_pmHDUFree(p_pmHDU *hdu);
+pmFPA *pmFPAAlloc(const psMetadata *camera // Camera configuration
+    );
+void p_pmFPAFree(pmFPA *fpa);
+
+pmChip *pmChipAlloc(pmFPA *fpa,	// FPA to which the chip belongs
+		    psString name	// Name of chip
+    );
+void p_pmChipFree(pmChip *chip);
+
+pmCell *pmCellAlloc(pmChip *chip,	// Chip to which the cell belongs
+		    psMetadata *cameraData, // Camera data
+		    psString name	// Name of cell
+    );
+void p_pmCellFree(pmCell *cell);
+
+pmReadout *pmReadoutAlloc(pmCell *cell, // Cell to which the readout belongs
+			  psImage *image, // The pixels
+			  psImage *mask,// The mask pixels
+			  int col0, int row0, int colBin, int rowBin // Data
+    );
+void p_pmReadoutFree(pmReadout *readout);
+
+// Select and exclude chips
+bool pmFPASelectChip(pmFPA *fpa, int chipNum);
+int pmFPAExcludeChip(pmFPA *fpa, int chipNum);
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.c	(revision 7462)
@@ -0,0 +1,1054 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+
+#include "papStuff.h"
+
+#include "pmFPAConceptsGet.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psMetadataItem *p_pmFPAConceptGetFromCamera(pmCell *cell, // The cell
+                                            const char *concept // Name of concept
+    )
+{
+    if (cell) {
+        psMetadata *camera = cell->camera;      // Camera data
+        // Need the CELL.NAME first, which should be in the cell->concepts since creation of the cell
+        bool mdStatus = true;           // Status of MD lookup
+        // Finally, the info that we want
+        psMetadataItem *item = psMetadataLookup(camera, concept);
+        return item;
+    }
+    return NULL;
+}
+
+
+psMetadataItem *p_pmFPAConceptGetFromHeader(pmFPA *fpa, // The FPA that contains the chip
+                                            pmChip *chip, // The chip that contains the cell
+                                            pmCell *cell, // The cell
+                                            const char *concept // Name of concept
+    )
+{
+    bool mdStatus = true;               // Status of MD lookup
+    psMetadata *translation = psMetadataLookupMD(&mdStatus, fpa->camera, "TRANSLATION"); // FITS translation
+    if (! mdStatus) {
+        psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
+        return NULL;
+    }
+
+    // Look for how to translate the concept into a FITS header name
+    const char *keyword = psMetadataLookupString(&mdStatus, translation, concept);
+    if (mdStatus && strlen(keyword) > 0) {
+        // We have a FITS header to look up --- search each level
+        if (cell && cell->hdu) {
+            psMetadataItem *cellItem = psMetadataLookup(cell->hdu->header, keyword);
+            if (cellItem) {
+                // XXX: Need to clean up before returning
+                return cellItem;
+            }
+        }
+
+        if (chip && chip->hdu) {
+            psMetadataItem *chipItem = psMetadataLookup(chip->hdu->header, keyword);
+            if (chipItem) {
+                // XXX: Need to clean up before returning
+                return chipItem;
+            }
+        }
+
+        if (fpa->hdu) {
+            psMetadataItem *fpaItem = psMetadataLookup(fpa->hdu->header, keyword);
+            if (fpaItem) {
+                // XXX: Need to clean up before returning
+                return fpaItem;
+            }
+        }
+
+        if (fpa->phu) {
+            psMetadataItem *fpaItem = psMetadataLookup(fpa->phu, keyword);
+            if (fpaItem) {
+                // XXX: Need to clean up before returning
+                return fpaItem;
+            }
+        }
+    }
+
+    // No header value
+    return NULL;
+}
+
+
+// Look for a default
+psMetadataItem *p_pmFPAConceptGetFromDefault(pmFPA *fpa, // The FPA that contains the chip
+                                             pmChip *chip, // The chip that contains the cell
+                                             pmCell *cell, // The cell
+                                             const char *concept // Name of concept
+    )
+{
+    bool mdOK = true;                   // Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
+    if (! mdOK) {
+        psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
+        return NULL;
+    }
+
+    psMetadataItem *defItem = psMetadataLookup(defaults, concept);
+    if (defItem) {
+        if (defItem->type == PS_DATA_METADATA) {
+            // A dependent default
+            psTrace(__func__, 7, "Evaluating dependent default....\n");
+            psMetadata *dependents = defItem->data.V; // The list of dependents
+            // Find out what it depends on
+            psString dependName = psStringCopy(concept);
+            psStringAppend(&dependName, ".DEPEND");
+            psString dependsOn = psMetadataLookupString(&mdOK, defaults, dependName);
+            if (! mdOK) {
+                psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
+                        " --- ignored\n", dependName);
+                // XXX: Need to clean up before returning
+                return NULL;
+            }
+            psFree(dependName);
+            // Find the value of the dependent concept
+            psMetadataItem *depItem = p_pmFPAConceptGetFromHeader(fpa, chip, cell, dependsOn);
+            if (! depItem) {
+                psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
+                        concept);
+                return NULL;
+            }
+            if (depItem->type != PS_DATA_STRING) {
+                psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
+                        " --- ignored.\n", dependsOn);
+            }
+
+            defItem = psMetadataLookup(dependents, depItem->data.V);    // This is now what we were after
+        }
+    }
+
+    // XXX: Need to clean up before returning
+    return defItem;                     // defItem is either NULL or points to what was desired
+}
+
+
+// Look for a database lookup
+// XXX: Not tested
+psMetadataItem *p_pmFPAConceptGetFromDB(pmFPA *fpa, // The FPA that contains the chip
+                                        pmChip *chip, // The chip that contains the cell
+                                        pmCell *cell, // The cell
+                                        psDB *db,       // DB handle
+                                        const char *concept // Name of concept
+    )
+{
+    if (! db) {
+        // No database initialised
+        return NULL;
+    }
+
+    bool mdStatus = true;               // Status of MD lookup
+    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
+    if (! mdStatus) {
+        // No error, because not everyone needs to use the DB
+        return NULL;
+    }
+
+    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept);
+    if (dbLookup) {
+        const char *tableName = psMetadataLookupString(&mdStatus, dbLookup, "TABLE"); // Name of the table
+        const char *colName = psMetadataLookupString(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+        const char *givenCols = psMetadataLookupString(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+                                                                                           // columns
+        const char *givenPS = psMetadataLookupString(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+                                                                                      // columns
+
+        // Now, need to get the "given"s
+        if (strlen(givenCols) || strlen(givenPS)) {
+            psList *cols = papSplit(givenCols, ",;"); // List of column names
+            psList *values = papSplit(givenPS, ",;"); // List of value names for the columns
+            psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+            if (cols->n != values->n) {
+                psLogMsg(__func__, PS_LOG_WARN, "The GIVENDBCOL and GIVENPS entries for %s do not have "
+                         "the same number of entries --- ignored.\n", concept);
+            } else {
+                // Iterators for the lists
+                psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+                psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+                char *column = NULL;    // Name of the column
+                while (column = psListGetAndIncrement(colsIter)) {
+                    char *name = psListGetAndIncrement(valuesIter); // Name for the value
+                    if (!strlen(column) || !strlen(name)) {
+                        psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
+                                 " empty --- ignored.\n", concept);
+                    } else {
+                        // Search for the value name
+                        psMetadataItem *item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, name);
+                        if (! item) {
+                            item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, name);
+                        }
+                        if (! item) {
+                            psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
+                                     " lookup on %s --- ignored.\n", name, concept);
+                        } else {
+                            // We need to create a new psMetadataItem.  I don't think we can't simply hack
+                            // the existing one, since that could conceivably cause memory leaks
+                            psMetadataItem *newItem = psMetadataItemAlloc(concept, item->type,
+                                                                          item->comment, item->data.V);
+                            psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+                            psFree(newItem);
+                        }
+                    }
+                    psFree(name);
+                    psFree(column);
+                } // Iterating through the columns
+                psFree(colsIter);
+                psFree(valuesIter);
+
+                psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+                // Note that we use limit=2 in order to test if there are multiple rows returned
+
+                psMetadataItem *result = NULL; // The final result of the DB lookup
+                if (dbResult->n == 0) {
+                    psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- ignored\n",
+                             concept);
+                } else {
+                    if (dbResult-> n > 1) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
+                                 " using the first one only.\n", concept);
+                    }
+                    result = (psMetadataItem*)dbResult->data[0];
+                }
+                // XXX: Need to clean up before returning
+                return result;
+            }
+            psFree(cols);
+            psFree(values);
+        }
+    } // Doing the "given"s.
+
+    psAbort(__func__, "Shouldn't ever get here.\n");
+}
+
+
+// Concept lookup
+psMetadataItem *p_pmFPAConceptGet(pmFPA *fpa, // The FPA
+                                  pmChip *chip,// The chip
+                                  pmCell *cell, // The cell
+                                  psDB *db, // DB handle
+                                  const char *concept // Concept name
+    )
+{
+    // Try headers, database, defaults in order
+    psMetadataItem *item = p_pmFPAConceptGetFromCamera(cell, concept);
+    if (! item) {
+        item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, concept);
+    }
+    if (! item) {
+        item = p_pmFPAConceptGetFromDB(fpa, chip, cell, db, concept);
+    }
+    if (! item) {
+        item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, concept);
+    }
+    return item; // item is either NULL, or points to what was desired
+}
+
+
+void p_pmFPAConceptGetF32(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    float value = NAN;
+    if (item) {
+        switch (item->type) {
+          case PS_DATA_F32:
+            value = item->data.F32;
+            break;
+          case PS_DATA_F64:
+            // Assume it's OK to truncate to floating point from double
+            value = (float)item->data.F64;
+            break;
+          case PS_DATA_S32:
+            // Promote to float
+            value = (float)item->data.S32;
+            break;
+          default:
+            psError(PS_ERR_IO, true, "Concept %s (%s) is not of floating point type (%x) --- treating as "
+                    "undefined.\n", name, comment, item->type);
+        }
+    } else {
+        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F32 | PS_META_REPLACE, comment, value);
+}
+
+void p_pmFPAConceptGetF64(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    double value = NAN;
+    if (item) {
+        switch (item->type) {
+          case PS_TYPE_F64:
+            value = item->data.F64;
+            break;
+          case PS_TYPE_F32:
+            // Promote to double
+            value = (double)item->data.F32;
+            break;
+          case PS_TYPE_S32:
+            // Promote to double
+            value = (double)item->data.S32;
+            break;
+          default:
+            psError(PS_ERR_IO, true, "Concept %s (%s) is not of double-precision floating point type (%x) "
+                    "--- treating as undefined.\n", name, comment, item->type);
+        }
+    } else {
+        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %f\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_F64 | PS_META_REPLACE, comment, value);
+}
+
+void p_pmFPAConceptGetS32(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    int value = 0;
+    if (item) {
+        switch (item->type) {
+          case PS_TYPE_S32:
+            value = item->data.S32;
+            break;
+          case PS_TYPE_F32:
+            psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) should be S32, but is F32 --- converting.\n",
+                     name, comment);
+            value = (int)item->data.F32;
+            break;
+          case PS_TYPE_F64:
+            psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) should be S32, but is F64 --- converting.\n",
+                     name, comment);
+            value = (int)item->data.F64;
+            break;
+          default:
+            psError(PS_ERR_IO, true, "Concept %s (%s) is not of integer type (%x) --- treating as "
+                    "undefined.\n", name, comment, item->type);
+        }
+    } else {
+        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %d\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_TYPE_S32 | PS_META_REPLACE, comment, value);
+}
+
+void p_pmFPAConceptGetString(pmFPA *fpa, // The FPA
+                             pmChip *chip, // The chip
+                             pmCell *cell, // The cell
+                             psDB *db,  // DB handle
+                             psMetadata *concepts, // The concepts MD
+                             const char *name, // Name of the concept
+                             const char *comment // Comment for concept
+    )
+{
+    psMetadataItem *item = p_pmFPAConceptGet(fpa, chip, cell, db, name);
+    psString value = NULL;
+    if (item) {
+        switch (item->type) {
+          case PS_DATA_STRING:
+            value = psMemIncrRefCounter(item->data.V);
+            break;
+          case PS_DATA_F32:
+            psStringAppend(&value, "%f", item->data.F32);
+            break;
+          case PS_DATA_S32:
+            psStringAppend(&value, "%d", item->data.S32);
+            break;
+          default:
+            psError(PS_ERR_IO, true, "Concept %s (%s) is not of string type (%x) --- treating as "
+                    "undefined.\n", name, comment, item->type);
+        }
+    } else {
+        psError(PS_ERR_IO, true, "Concept %s (%s) is not defined.\n", name, comment);
+        value = psStringCopy("");
+    }
+    psTrace(__func__, 7, "Adding %s (%s): %s\n", name, comment, value);
+
+    psMetadataAdd(concepts, PS_LIST_TAIL, name, PS_DATA_STRING | PS_META_REPLACE, comment, value);
+    psFree(value);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Ingest concepts for the FPA
+void pmFPAIngestConcepts(pmFPA *fpa,    // The FPA
+                         psDB *db       // DB handle
+    )
+{
+    if (! fpa->concepts) {
+        fpa->concepts = psMetadataAlloc();
+    }
+
+    // FPA.NAME
+    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.NAME", "Name of FPA");
+
+    // FPA.AIRMASS
+    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.AIRMASS", "Airmass at boresight");
+
+    // FPA.FILTER
+    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.FILTER", "Filter used");
+
+    // FPA.POSANGLE
+    p_pmFPAConceptGetF32(fpa, NULL, NULL, db, fpa->concepts, "FPA.POSANGLE", "Position angle for instrument");
+
+    // FPA.RADECSYS
+    p_pmFPAConceptGetString(fpa, NULL, NULL, db, fpa->concepts, "FPA.RADECSYS", "Celestial coordinate system");
+
+    // These take some extra work
+
+    // FPA.RA
+    {
+        double ra = NAN;                // The RA
+        psMetadataItem *raItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.RA"); // The FPA.RA item
+        if (raItem) {
+            switch (raItem->type) {
+              case PS_TYPE_F32:
+                ra = raItem->data.F32;
+                break;
+              case PS_TYPE_F64:
+                ra = raItem->data.F64;
+                break;
+              case PS_DATA_STRING:
+                // Sexagesimal format
+                {
+                    int big, medium;
+                    float small;
+                    // XXX: Upgrade path is to allow dd:mm.mmm
+                    if (sscanf(raItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+                        sscanf(raItem->data.V, "%d %d %f", &big, &medium, &small) != 3) {
+                        psError(PS_ERR_IO, true, "Cannot interpret FPA.RA: %s\n", raItem->data.V);
+                        break;
+                    }
+                    ra = abs(big) + (float)medium/60.0 + small/3600.0;
+                    if (big < 0) {
+                        ra *= -1.0;
+                    }
+                }
+                break;
+              default:
+                psError(PS_ERR_IO, true, "FPA.RA is of an unexpected type: %x\n", raItem->type);
+            }
+
+            // How to interpret the RA
+            const psMetadata *camera = fpa->camera; // Camera configuration data
+            bool mdok = true;           // Status of MD lookup
+            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+            if (mdok && formats) {
+                psString raFormat = psMetadataLookupString(&mdok, formats, "FPA.RA");
+                if (mdok && strlen(raFormat) > 0) {
+                    if (strcasecmp(raFormat, "HOURS") == 0) {
+                        ra *= M_PI / 12.0;
+                    } else if (strcasecmp(raFormat, "DEGREES") == 0) {
+                        ra *= M_PI / 180.0;
+                    } else if (strcasecmp(raFormat, "RADIANS") == 0) {
+                        // No action required
+                    } else {
+                        psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.RA in FORMATS (%s) --- assuming"
+                                 " HOURS.\n");
+                        ra *= M_PI / 12.0;
+                    }
+                } else {
+                    psError(PS_ERR_IO, false, "Unable to find FPA.RA in FORMATS --- assuming HOURS.\n");
+                    ra *= M_PI / 12.0;
+                }
+            } else {
+                psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+                        "assuming format for FPA.RA is HOURS.\n");
+                ra *= M_PI / 12.0;
+            }
+        } else {
+            psError(PS_ERR_IO, false, "Couldn't find FPA.RA.\n");
+        }
+
+        psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_DATA_F64 | PS_META_REPLACE,
+                      "Right Ascension of the boresight (radians)", ra);
+
+    }
+
+    // FPA.DEC
+    {
+        double dec = NAN;               // The DEC
+        psMetadataItem *decItem = p_pmFPAConceptGet(fpa, NULL, NULL, db, "FPA.DEC"); // The FPA.DEC item
+        if (decItem) {
+            switch (decItem->type) {
+              case PS_TYPE_F32:
+                dec = decItem->data.F32;
+                break;
+              case PS_TYPE_F64:
+                dec = decItem->data.F64;
+                break;
+              case PS_DATA_STRING:
+                // Sexagesimal format
+                {
+                    int big, medium;
+                    float small;
+                    // XXX: Upgrade path is to allow dd:mm.mmm
+                    if (sscanf(decItem->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+                        sscanf(decItem->data.V, "%d %d %f", &big, &medium, &small) != 3) {
+                        psError(PS_ERR_IO, true, "Cannot interpret FPA.DEC: %s\n", decItem->data.V);
+                        break;
+                    }
+                    dec = abs(big) + (float)medium/60.0 + small/3600.0;
+                    if (big < 0) {
+                        dec *= -1.0;
+                    }
+                }
+                break;
+              default:
+                psError(PS_ERR_IO, true, "FPA.DEC is of an unexpected type: %x\n", decItem->type);
+            }
+
+            // How to interpret the DEC
+            const psMetadata *camera = fpa->camera; // Camera configuration data
+            bool mdok = true;           // Status of MD lookup
+            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+            if (mdok && formats) {
+                psString decFormat = psMetadataLookupString(&mdok, formats, "FPA.DEC");
+                if (mdok && strlen(decFormat) > 0) {
+                    if (strcasecmp(decFormat, "HOURS") == 0) {
+                        dec *= M_PI / 12.0;
+                    } else if (strcasecmp(decFormat, "DEGREES") == 0) {
+                        dec *= M_PI / 180.0;
+                    } else if (strcasecmp(decFormat, "RADIANS") == 0) {
+                        // No action required
+                    } else {
+                        psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.DEC in FORMATS (%s) --- "
+                                 "assuming DEGREES.\n");
+                        dec *= M_PI / 180.0;
+                    }
+                } else {
+                    psError(PS_ERR_IO, false, "Unable to find FPA.DEC in FORMATS --- assuming DEGREES.\n");
+                    dec *= M_PI / 180.0;
+                }
+            } else {
+                psError(PS_ERR_IO, false, "Unable to find FORMATS metadata in camera configuration --- "
+                        "assuming format for FPA.DEC is DEGREES.\n");
+                dec *= M_PI / 180.0;
+            }
+        } else {
+            psError(PS_ERR_IO, false, "Couldn't find FPA.DEC.\n");
+        }
+
+        psMetadataAdd(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_DATA_F64 | PS_META_REPLACE,
+                      "Declination of the boresight (radians)", dec);
+
+    }
+
+    // Pau.
+}
+
+
+// Ingest concepts for the chip
+bool pmChipIngestConcepts(pmChip *chip, // The chip
+                          psDB *db      // DB handle
+    )
+{
+    pmFPA *fpa = chip->parent;          // The parent FPA
+
+    if (! chip->concepts) {
+        chip->concepts = psMetadataAlloc();
+    }
+
+    // CHIP.NAME --- added by pmFPAConstruct
+
+    // Pau.
+}
+
+
+// Add corrective to a position --- in case the user wants FORTRAN indexing (like the FITS standard...)
+static bool correctPosition(pmFPA *fpa, // FPA, contains the camera configuration
+                            pmCell *cell, // Cell containing the concept to correct
+                            const char *conceptName // Name of concept to correct
+    )
+{
+    bool mdok = false;              // Result of MD lookup
+    psMetadata *formats = psMetadataLookupMD(&mdok, fpa->camera, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupString(&mdok, formats, conceptName);
+        if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
+            psMetadataItem *valueItem = psMetadataLookup(cell->concepts, conceptName);
+            valueItem->data.S32 -= 1;
+        }
+    }
+    return true;
+}
+
+bool p_pmCellIngestConcept(pmFPA *fpa,  // The FPA
+                           pmChip *chip, // The chip
+                           pmCell *cell, // The cell
+                           psDB *db,    // DB handle
+                           const char *concept // Name of the concept
+    )
+{
+    if (strcmp(concept, "CELL.GAIN") == 0) {
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.GAIN", "CCD gain (e/count)");
+    } else if (strcmp(concept, "CELL.READNOISE") == 0) {
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.READNOISE", "CCD read noise (e)");
+    } else if (strcmp(concept, "CELL.SATURATION") == 0) {
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.SATURATION",
+                             "Saturation level (ADU)");
+    } else if (strcmp(concept, "CELL.BAD") == 0) {
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.BAD", "Bad level (ADU)");
+    } else if (strcmp(concept, "CELL.XPARITY") == 0) {
+        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.XPARITY",
+                             "Orientation in x compared to the rest of the FPA");
+    } else if (strcmp(concept, "CELL.YPARITY") == 0) {
+        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.YPARITY",
+                             "Orientation in y compared to the rest of the FPA");
+    } else if (strcmp(concept, "CELL.READDIR") == 0) {
+        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.READDIR",
+                             "Read direction: 1=row, 2=col");
+    } else if (strcmp(concept, "CELL.EXPOSURE") == 0) { // used to be READOUT.EXPOSURE
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.EXPOSURE", "Exposure time (sec)");
+    } else if (strcmp(concept, "CELL.DARKTIME") == 0) { // used to be READOUT.DARKTIME
+        p_pmFPAConceptGetF32(fpa, chip, cell, db, cell->concepts, "CELL.DARKTIME",
+                             "Time since CCD flush (sec)");
+        // These take some extra work
+    } else if (strcmp(concept, "CELL.TRIMSEC") == 0) {
+        psRegion *trimsec = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed by value)
+
+        psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC");
+        if (! secItem) {
+            psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.\n");
+            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+        } else if (secItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_IO, true, "CELL.TRIMSEC is not of type STR (%x)\n", secItem->type);
+            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+        } else {
+            psString section = secItem->data.V; // The section string
+
+            psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC.SOURCE");
+            if (! sourceItem) {
+                psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.SOURCE.\n");
+                *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+            } else if (sourceItem->type != PS_DATA_STRING) {
+                psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+                *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+            } else {
+                psString source = sourceItem->data.V; // The source string
+
+                if (strcasecmp(source, "VALUE") == 0) {
+                    *trimsec = psRegionFromString(section);
+                } else if (strcasecmp(source, "HEADER") == 0) {
+                    psMetadata *header = NULL; // The FITS header
+                    if (cell->hdu) {
+                        header = cell->hdu->header;
+                    } else if (chip->hdu) {
+                        header = chip->hdu->header;
+                    } else if (fpa->hdu) {
+                        header = fpa->hdu->header;
+                    }
+                    if (! header) {
+                        psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+                        *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+                    } else {
+                        bool mdok = true;               // Status of MD lookup
+                        psString secValue = psMetadataLookupString(&mdok, header, section);
+                        if (! mdok || ! secValue) {
+                            psError(PS_ERR_IO, false, "Unable to locate header %s\n", section);
+                            *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+                        } else {
+                            *trimsec = psRegionFromString(secValue);
+                        }
+                    }
+                } else {
+                    psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
+                            "VALUE.\n", source);
+                    *trimsec = psRegionFromString(section);
+                } // Value of CELL.TRIMSEC.SOURCE
+            } // Looking up CELL.TRIMSEC.SOURCE
+        } // Looking up CELL.TRIMSEC
+
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_DATA_UNKNOWN | PS_META_REPLACE,
+                      "Trim section", trimsec);
+        psFree(trimsec);
+
+    } else if (strcmp(concept, "CELL.BIASSEC") == 0) {
+        psList *biassecs = psListAlloc(NULL); // List of bias sections
+
+        psMetadataItem *secItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC");
+        if (! secItem) {
+            psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.\n");
+        } else if (secItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_IO, true, "CELL.BIASSEC is not of type STR (%x)\n", secItem->type);
+        } else {
+            psString sections = secItem->data.V; // The section string
+
+            psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC.SOURCE");
+            if (! sourceItem) {
+                psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.SOURCE.\n");
+            } else if (sourceItem->type != PS_DATA_STRING) {
+                psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+            } else {
+                psString source = sourceItem->data.V; // The source string
+
+                psList *secList = papSplit(sections, " ;"); // List of sections
+                psListIterator *secIter = psListIteratorAlloc(secList, PS_LIST_HEAD, false); // Iterator over
+                                                                                             // sections
+                psString aSection = NULL; // A section from the list
+                while (aSection = psListGetAndIncrement(secIter)) {
+                    psRegion *region = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed
+                                                                  // by value)
+
+                    if (strcasecmp(source, "VALUE") == 0) {
+                        *region = psRegionFromString(aSection);
+                    } else if (strcasecmp(source, "HEADER") == 0) {
+                        psMetadata *header = NULL; // The FITS header
+                        if (cell->hdu) {
+                            header = cell->hdu->header;
+                        } else if (chip->hdu) {
+                            header = chip->hdu->header;
+                        } else if (fpa->hdu) {
+                            header = fpa->hdu->header;
+                        }
+                        if (! header) {
+                            psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+                            *region = psRegionSet(0.0,0.0,0.0,0.0);
+                        } else {
+                            bool mdok = true;           // Status of MD lookup
+                            psString secValue = psMetadataLookupString(&mdok, header, aSection);
+                            if (! mdok || ! secValue) {
+                                psError(PS_ERR_IO, false, "Unable to locate header %s\n", aSection);
+                                *region = psRegionSet(0.0,0.0,0.0,0.0);
+                            } else {
+                                *region = psRegionFromString(secValue);
+                            }
+                        }
+                    } else {
+                        psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE (%s) is not HEADER or VALUE --- trying "
+                                "VALUE.\n", source);
+                        *region = psRegionFromString(aSection);
+                    } // Value of CELL.BIASSEC.SOURCE
+
+                    psListAdd(biassecs, PS_LIST_TAIL, region);
+                    psFree(region);
+                } // Iterating over multiple sections
+                psFree(secIter);
+                psFree(secList);
+            } // Looking up CELL.BIASSEC.SOURCE
+        } // Looking up CELL.BIASSEC
+
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_DATA_LIST | PS_META_REPLACE,
+                      "Bias sections", biassecs);
+        psFree(biassecs);
+
+    } else if (strcmp(concept, "CELL.XBIN") == 0) {
+        int xBin = 1;                   // Binning factor in x
+        psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.XBIN");
+        if (! binItem) {
+            psError(PS_ERR_IO, false, "Couldn't find CELL.XBIN.\n");
+        } else if (binItem->type == PS_DATA_STRING) {
+            psString binString = binItem->data.V; // The string containing the binning
+            if (sscanf(binString, "%d %*d", &xBin) != 1 &&
+                sscanf(binString, "%d,%*d", &xBin) != 1) {
+                psError(PS_ERR_IO, true, "Unable to read string to get x binning: %s\n", binString);
+            }
+        } else if (binItem->type == PS_TYPE_S32) {
+            xBin = binItem->data.S32;
+        } else {
+            psError(PS_ERR_IO, true, "Note sure how to interpret CELL.XBIN of type %x --- assuming 1.\n",
+                    binItem->type);
+        }
+
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_TYPE_S32 | PS_META_REPLACE,
+                      "Binning in x", xBin);
+
+    } else if (strcmp(concept, "CELL.YBIN") == 0) {
+        int yBin = 1;                   // Binning factor in y
+        psMetadataItem *binItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.YBIN");
+        if (! binItem) {
+            psError(PS_ERR_IO, false, "Couldn't find CELL.YBIN.\n");
+        } else if (binItem->type == PS_DATA_STRING) {
+            psString binString = binItem->data.V; // The string containing the binning
+            if (sscanf(binString, "%*d %d", &yBin) != 1 &&
+                sscanf(binString, "%*d,%d", &yBin) != 1) {
+                psError(PS_ERR_IO, true, "Unable to read string to get y binning: %s\n", binString);
+            }
+        } else if (binItem->type == PS_TYPE_S32) {
+            yBin = binItem->data.S32;
+        } else {
+            psError(PS_ERR_IO, true, "Note sure how to interpret CELL.YBIN of type %x --- assuming 1.\n",
+                    binItem->type);
+        }
+
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_TYPE_S32 | PS_META_REPLACE,
+                      "Binning in y", yBin);
+
+    } else if (strcmp(concept, "CELL.TIME") == 0 || strcmp(concept, "CELL.TIMESYS") == 0) {
+        // Do CELL.TIME and CELL.TIMESYS together
+        psTime *time = NULL;            // The time
+        psTimeType timeSys = PS_TIME_UTC; // The time system
+
+        // CELL.TIMESYS
+        psMetadataItem *sysItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIMESYS");
+        if (! sysItem) {
+            psError(PS_ERR_IO, true, "Couldn't find CELL.TIMESYS --- assuming UTC.\n");
+        } else if (sysItem->type != PS_DATA_STRING) {
+            psError(PS_ERR_IO, true, "CELL.TIMESYS isn't of type STRING --- assuming UTC.\n");
+        } else {
+            psString sys = sysItem->data.V; // The time system string
+            if (strcasecmp(sys, "TAI") == 0) {
+                timeSys = PS_TIME_TAI;
+            } else if (strcasecmp(sys, "UTC") == 0) {
+                timeSys = PS_TIME_UTC;
+            } else if (strcasecmp(sys, "UT1") == 0) {
+                timeSys = PS_TIME_UT1;
+            } else if (strcasecmp(sys, "TT") == 0) {
+                timeSys = PS_TIME_TT;
+            } else {
+                psError(PS_ERR_IO, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
+            }
+        }
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_TYPE_S32 | PS_META_REPLACE,
+                      "Time system", timeSys);
+
+        psMetadataItem *timeItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TIME");
+        if (! timeItem) {
+            psError(PS_ERR_IO, false, "Couldn't find CELL.TIME.\n");
+        } else {
+            // Get format
+            const psMetadata *camera = fpa->camera; // The camera configuration data
+            bool mdok = true;           // Status of MD lookup
+            psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+            if (mdok && formats) {
+                psString timeFormat = psMetadataLookupString(&mdok, formats, "CELL.TIME");
+                if (mdok && strlen(timeFormat) > 0) {
+                    switch (timeItem->type) {
+                      case PS_DATA_STRING:
+                        {
+                            psString timeString = timeItem->data.V;     // String with the time
+                            if (strcasecmp(timeFormat, "ISO") == 0) {
+                                // timeString contains an ISO time
+                                time = psTimeFromISO(timeString, timeSys);
+                            } else if (strstr(timeFormat, "SEPARATE")) {
+                                // timeString contains headers for the date and time
+                                psMetadata *header = NULL; // The FITS header
+                                if (cell->hdu) {
+                                    header = cell->hdu->header;
+                                } else if (chip->hdu) {
+                                    header = chip->hdu->header;
+                                } else if (fpa->hdu) {
+                                    header = fpa->hdu->header;
+                                }
+                                if (! header) {
+                                    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+                                } else {
+                                    // Get the headers
+                                    char *stuff1 = strpbrk(timeString, " ,;");
+                                    psString dateName = psStringNCopy(timeString,
+                                                                      strlen(timeString) - strlen(stuff1));
+                                    char *stuff2 = strpbrk(stuff1, "abcdefghijklmnopqrstuvwxyz"
+                                                           "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+                                    psString timeName = psStringCopy(stuff2);
+
+                                    bool mdok = true; // Status of MD lookup
+                                    psString dateString = psMetadataLookupString(&mdok, header, dateName);
+                                    psFree(dateName);
+                                    int day = 0, month = 0, year = 0;
+                                    if (sscanf(dateString, "%d-%d-%d", &day, &month, &year) != 3 &&
+                                        sscanf(dateString, "%d/%d/%d", &day, &month, &year) != 3) {
+                                        psError(PS_ERR_IO, true, "Unable to read date: %s\n", dateString);
+                                    } else {
+                                        if (strstr(timeFormat, "BACKWARDS")) {
+                                            int temp = day;
+                                            day = year;
+                                            year = temp;
+                                        }
+                                        if (strstr(timeFormat, "PRE2000") || year < 2000) {
+                                            year += 2000;
+                                        }
+
+                                        psMetadataItem *timeItem = psMetadataLookup(header, timeName);
+                                        if (! timeItem) {
+                                            psError(PS_ERR_IO, false, "Unable to find time header: %s\n",
+                                                    timeName);
+                                        } else if (timeItem->type == PS_DATA_STRING) {
+                                            // Time is a string, in the usual way:
+                                            psStringAppend(&dateString, "T%s", timeItem->data.V);
+                                        } else {
+                                            // Assume that time is specified in Second of Day
+                                            double seconds = NAN;
+                                            switch (timeItem->type) {
+                                              case PS_TYPE_S32:
+                                                seconds = timeItem->data.S32;
+                                                break;
+                                              case PS_TYPE_F32:
+                                                seconds = timeItem->data.F32;
+                                                break;
+                                              case PS_TYPE_F64:
+                                                seconds = timeItem->data.F64;
+                                                break;
+                                              default:
+                                                psError(PS_ERR_IO, true, "Time header (%s) is not of an "
+                                                        "expected type: %x\n", timeName, timeItem->type);
+                                            }
+                                            // Now print to timeString as "hh:mm:ss.ss"
+                                            int hours = seconds / 3600;
+                                            seconds -= (double)hours * 3600.0;
+                                            int minutes = seconds / 60;
+                                            seconds -= (double)minutes * 60.0;
+                                            psStringAppend(&dateString, "T%02d:%02d:%02f", hours, minutes,
+                                                           seconds);
+                                        }
+                                        time = psTimeFromISO(dateString, timeSys);
+                                    } // Reading date and time
+                                    psFree(timeName);
+                                } // Reading headers
+                            } else {
+                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%s) --- trying "
+                                        "ISO\n", timeString);
+                                time = psTimeFromISO(timeString, timeSys);
+                            } // Interpreting the time string
+                        }
+                        break;
+                      case PS_TYPE_F32:
+                        {
+                            double timeValue = (double)timeItem->data.F32;
+                            if (strcasecmp(timeFormat, "JD") == 0) {
+                                time = psTimeFromJD(timeValue);
+                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
+                                time = psTimeFromMJD(timeValue);
+                            } else {
+                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
+                                        "JD\n", timeValue);
+                                time = psTimeFromJD(timeValue);
+                            }
+                        }
+                        break;
+                      case PS_TYPE_F64:
+                        {
+                            double timeValue = (double)timeItem->data.F64;
+                            if (strcasecmp(timeFormat, "JD") == 0) {
+                                time = psTimeFromJD(timeValue);
+                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
+                                time = psTimeFromMJD(timeValue);
+                            } else {
+                                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
+                                        "JD\n", timeValue);
+                                time = psTimeFromJD(timeValue);
+                            }
+                        }
+                        break;
+                      default:
+                        psError(PS_ERR_IO, true, "Unable to parse CELL.TIME.\n");
+                    }
+                } else {
+                    psError(PS_ERR_IO, false, "Unable to find CELL.TIME in FORMATS.\n");
+                } // Getting the format
+            } else {
+                psError(PS_ERR_IO, false, "Unable to find FORMATS in camera configuration.\n");
+            } // Getting the formats
+        } // Getting CELL.TIME
+
+        psMetadataAdd(cell->concepts, PS_LIST_TAIL, "CELL.TIME", PS_DATA_TIME | PS_META_REPLACE,
+                      "Time of exposure", time);
+        psFree(time);
+
+        // These are new and experimental concepts: CELL.X0 and CELL.Y0
+    } else if (strcmp(concept, "CELL.X0") == 0) {
+        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.X0", "Position of (0,0) on the chip");
+        correctPosition(fpa, cell, "CELL.X0");
+    } else if (strcmp(concept, "CELL.Y0") == 0) {
+        p_pmFPAConceptGetS32(fpa, chip, cell, db, cell->concepts, "CELL.Y0", "Position of (0,0) on the chip");
+        correctPosition(fpa, cell, "CELL.Y0");
+    }
+
+    return true;
+}
+
+
+
+// Ingest concepts for the cell
+bool pmCellIngestConcepts(pmCell *cell, // The cell
+                          psDB *db      // DB handle
+    )
+{
+    pmChip *chip = cell->parent;        // The parent chip
+    pmFPA *fpa = chip->parent;          // The parent FPA
+
+    if (! cell->concepts) {
+        cell->concepts = psMetadataAlloc();
+    }
+
+    // CELL.NAME --- added by pmFPAConstruct
+
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.GAIN");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.READNOISE");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.SATURATION");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.BAD");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.XPARITY");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.YPARITY");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.READDIR");
+
+    // These used to be pmReadoutGetExposure and pmReadoutGetDarkTime, but that doesn't really make sense at
+    // the moment.  Maybe we need to add a "parent" link to the readouts.  But then how are the exposure times
+    // REALLY derived?  They're not in the FITS headers, because a readout is a plane in a 3D image.  We'll
+    // have to dream up some additional suffix to specify these, but for now....
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.EXPOSURE");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.DARKTIME");
+
+
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.TRIMSEC");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.BIASSEC");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.TIME"); // This also does CELL.TIMESYS
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.XBIN");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.YBIN");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.X0");
+    p_pmCellIngestConcept(fpa, chip, cell, db, "CELL.Y0");
+
+}
+
+
+// Retrieve a concept
+psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
+                                 const char *concept // The concept
+    )
+{
+    psMetadataItem *item = psMetadataLookup(cell->concepts, concept);
+    if (! item) {
+        pmChip *chip = cell->parent;
+        item = psMetadataLookup(chip->concepts, concept);
+        if (! item) {
+            pmFPA *fpa = chip->parent;
+            item = psMetadataLookup(fpa->concepts, concept);
+        }
+    }
+    return item;                        // item is either NULL or is what we want.
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsGet.h	(revision 7462)
@@ -0,0 +1,94 @@
+#ifndef PM_FPA_CONCEPTS_GET_H
+#define PM_FPA_CONCEPTS_GET_H
+
+#include "pmFPA.h"
+#include "pslib.h"
+#include "psAdditionals.h"
+
+psMetadataItem *p_pmFPAConceptGetFromCamera(pmCell *cell, // The cell
+                                            const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromHeader(pmFPA *fpa, // The FPA that contains the chip
+                                            pmChip *chip, // The chip that contains the cell
+                                            pmCell *cell, // The cell
+                                            const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromDefault(pmFPA *fpa, // The FPA that contains the chip
+                                             pmChip *chip, // The chip that contains the cell
+                                             pmCell *cell, // The cell
+                                             const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGetFromDB(pmFPA *fpa, // The FPA that contains the chip
+                                        pmChip *chip, // The chip that contains the cell
+                                        pmCell *cell, // The cell
+                                        psDB *db,       // DB handle
+                                        const char *concept // Name of concept
+    );
+psMetadataItem *p_pmFPAConceptGet(pmFPA *fpa, // The FPA
+                                  pmChip *chip,// The chip
+                                  pmCell *cell, // The cell
+                                  psDB *db, // DB handle
+                                  const char *concept // Concept name
+    );
+void p_pmFPAConceptGetF32(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetF64(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetS32(pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          psDB *db,     // DB handle
+                          psMetadata *concepts, // The concepts MD
+                          const char *name, // Name of the concept
+                          const char *comment // Comment for concept
+    );
+void p_pmFPAConceptGetString(pmFPA *fpa, // The FPA
+                             pmChip *chip, // The chip
+                             pmCell *cell, // The cell
+                             psDB *db,  // DB handle
+                             psMetadata *concepts, // The concepts MD
+                             const char *name, // Name of the concept
+                             const char *comment // Comment for concept
+    );
+
+
+// Ingest a single cell concept; this is the workhorse for ingesting cell concepts
+bool p_pmCellIngestConcept(pmFPA *fpa,  // The FPA
+                           pmChip *chip, // The chip
+                           pmCell *cell, // The cell
+                           psDB *db,    // DB handle
+                           const char *concept // Name of the concept
+    );
+
+// Ingest concepts for the FPA
+void pmFPAIngestConcepts(pmFPA *fpa,    // The FPA
+                         psDB *db       // DB handle
+    );
+// Ingest concepts for the chip
+bool pmChipIngestConcepts(pmChip *chip, // The chip
+                          psDB *db      // DB handle
+    );
+// Ingest concepts for the cell
+bool pmCellIngestConcepts(pmCell *cell, // The cell
+                          psDB *db      // DB handle
+    );
+
+// Retrieve a concept
+psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
+                                 const char *concept // The concept
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.c	(revision 7462)
@@ -0,0 +1,887 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+
+#include "papStuff.h"
+
+#include "pmFPAConceptsGet.h"
+#include "pmFPAConceptsSet.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+#define COMPARE_REGIONS(a,b) (((a)->x0 == (b)->x0 && \
+                               (a)->x1 == (b)->x1 && \
+                               (a)->y0 == (b)->y0 && \
+                               (a)->y1 == (b)->y1) ? true : false)
+
+
+static bool compareConcepts(psMetadataItem *item1, // First item to compare
+			    psMetadataItem *item2 // Second item to compare
+    )
+{
+    // First order checks
+    if (! item1 || ! item2) {
+	return false;
+    }
+    if (strcasecmp(item1->name, item2->name) != 0) {
+	return false;
+    }
+
+    // Check the more boring types
+    switch (item1->type) {
+      case PS_TYPE_S32:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.S32 == item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.S32 == (int)item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.S32 == (int)item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+      case PS_TYPE_F32:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.F32 == (float)item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.F32 == item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.F32 == (float)item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+      case PS_TYPE_F64:
+	switch (item2->type) {
+	  case PS_TYPE_S32:
+	    return (item1->data.F64 == (double)item2->data.S32) ? true : false;
+	  case PS_TYPE_F32:
+	    return (item1->data.F64 == (double)item2->data.F32) ? true : false;
+	  case PS_TYPE_F64:
+	    return (item1->data.F64 == item2->data.F64) ? true : false;
+	  default:
+	    return false;
+	}
+	return (item1->data.F64 == item2->data.F64) ? true : false;
+      case PS_DATA_STRING:
+	if (item2->type != PS_DATA_STRING) {
+	    return false;
+	}
+	return (strcasecmp(item1->data.V, item2->data.V) == 0) ? true : false;
+      default:
+	return false;
+    }
+    psAbort(__func__, "Should never get here.\n");
+}
+
+
+// Well, not really "set", but check to make sure it's there and matches
+static bool setConceptInCamera(pmCell *cell, // The cell
+			       psMetadataItem *concept // Concept
+    )
+{
+    if (cell) {
+	psMetadataItem *item = psMetadataLookup(cell->camera, concept->name); // Info we want
+	return compareConcepts(item, concept);
+    }
+}   
+
+
+static bool setConceptInHeader(pmFPA *fpa, // The FPA that contains the chip
+			       pmChip *chip, // The chip that contains the cell
+			       pmCell *cell, // The cell
+			       psMetadataItem *concept // Concept
+    )
+{
+    bool mdok = true;			// Status of MD lookup
+    bool status = false;		// Status of setting header
+    psMetadata *translation = psMetadataLookupMD(&mdok, fpa->camera, "TRANSLATION"); // FITS translation
+    if (! mdok) {
+	psError(PS_ERR_IO, false, "Unable to find TRANSLATION in camera configuration.\n");
+	return false;
+    }
+
+    // Look for how to translate the concept into a FITS header name
+    const char *keyword = psMetadataLookupString(&mdok, translation, concept->name);
+    if (mdok && strlen(keyword) > 0) {
+	psTrace(__func__, 6, "It's in keyword %s\n", keyword);
+	psMetadataItem *headerItem = NULL; // Item to add to header
+	// XXX: Need to expand range of types
+	switch (concept->type) {
+	  case PS_DATA_STRING:
+	    headerItem = psMetadataItemAllocStr(keyword, concept->comment, concept->data.V);
+	    break;
+	  case PS_DATA_S32:
+	    headerItem = psMetadataItemAllocS32(keyword, concept->comment, concept->data.S32);
+	    break;
+	  case PS_DATA_F32:
+	    headerItem = psMetadataItemAllocF32(keyword, concept->comment, concept->data.F32);
+	    break;
+	  case PS_DATA_F64:
+	    headerItem = psMetadataItemAllocF64(keyword, concept->comment, concept->data.F64);
+	    break;
+	  default:
+	    headerItem = psMetadataItemAlloc(keyword, concept->type, concept->comment,
+					     concept->data.V); // Item for the header
+	}
+
+	// We have a FITS header to look up --- search each level
+	if (cell && cell->hdu) {
+	    psTrace(__func__, 7, "Adding to the cell level header...\n");
+	    psMetadataAddItem(cell->hdu->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+	    status = true;
+	} else if (chip && chip->hdu) {
+	    psTrace(__func__, 7, "Adding to the chip level header...\n");
+	    psMetadataAddItem(chip->hdu->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+	    status = true;
+	} else if (fpa->hdu) {
+	    psTrace(__func__, 7, "Adding to the FPA level header...\n");
+	    psMetadataAddItem(fpa->hdu->header, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+	    status = true;
+	} else {
+	    // In desperation, add to the PHU --- it HAS to be in the header somewhere!
+	    if (! fpa->phu) {
+		fpa->phu = psMetadataAlloc();
+	    }
+	    psTrace(__func__, 7, "Adding to the PHU...\n");
+	    psMetadataAddItem(fpa->phu, headerItem, PS_LIST_TAIL, PS_META_REPLACE);
+	    status = true;
+	}
+	psFree(headerItem);
+    }
+
+    // No header value
+    return status;
+}
+
+
+// Well, not really "set", but check to see if it's there, and matches
+static bool setConceptInDefault(pmFPA *fpa, // The FPA that contains the chip
+				pmChip *chip, // The chip that contains the cell
+				pmCell *cell, // The cell
+				psMetadataItem *concept // Concept
+    )
+{
+    bool mdOK = true;			// Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdOK, fpa->camera, "DEFAULTS");
+    if (! mdOK || ! defaults) {
+	psError(PS_ERR_IO, false, "Unable to find DEFAULTS in camera configuration.\n");
+	return false;
+    }
+
+    psMetadataItem *defItem = psMetadataLookup(defaults, concept->name);
+    bool status = false;		// Result of checking the database
+    if (defItem) {
+	if (defItem->type == PS_DATA_METADATA) {
+	    // A dependent default
+	    psTrace(__func__, 7, "Evaluating dependent default....\n");
+	    psMetadata *dependents = defItem->data.V; // The list of dependents
+	    // Find out what it depends on
+	    psString dependName = psStringCopy(concept->name);
+	    psStringAppend(&dependName, ".DEPEND");
+	    psString dependsOn = psMetadataLookupString(&mdOK, defaults, dependName);
+	    if (! mdOK) {
+		psError(PS_ERR_IO, false, "Unable to find %s in camera configuration for dependent default"
+			" --- ignored\n", dependName);
+		// XXX: Need to clean up before returning
+		return false;
+	    }
+	    psFree(dependName);
+	    // Find the value of the dependent concept
+	    psMetadataItem *depItem = p_pmFPAConceptGetFromHeader(fpa, chip, cell, dependsOn);
+	    if (! depItem) {
+		psError(PS_ERR_IO, true, "Unable to find value for %s (required for %s)\n", dependsOn,
+			concept->name);
+		return false;
+	    }
+	    if (depItem->type != PS_DATA_STRING) {
+		psError(PS_ERR_IO, true, "Value of %s is not of type string, as required for dependency"
+			" --- ignored.\n", dependsOn);
+	    }
+	    
+	    defItem = psMetadataLookup(dependents, depItem->data.V); // This is now what we were after
+	}
+
+	status = compareConcepts(defItem, concept);
+	if (! status) {
+	    psError(PS_ERR_IO, true, "Concept %s is specified by default in the camera configuration, "
+		    "but doesn't match the actual value.\n", concept->name);
+	}
+    }
+
+    // XXX: Need to clean up before returning
+    return status;
+}
+
+
+// XXX: Not tested at all
+static bool setConceptInDB(pmFPA *fpa, // The FPA that contains the chip
+			   pmChip *chip, // The chip that contains the cell
+			   pmCell *cell, // The cell
+			   psDB *db,	// DB handle
+			   psMetadataItem *concept // Concept
+    )
+{
+    if (! db) {
+	// No database initialised
+	return false;
+    }
+
+    bool mdStatus = true;		// Status of MD lookup
+    psMetadata *database = psMetadataLookupMD(&mdStatus, fpa->camera, "DATABASE");
+    if (! mdStatus) {
+	// No error, because not everyone needs to use the DB
+	return NULL;
+    }
+
+    psMetadata *dbLookup = psMetadataLookupMD(&mdStatus, database, concept->name);
+    if (dbLookup) {
+	const char *tableName = psMetadataLookupString(&mdStatus, dbLookup, "TABLE"); // Name of the table
+	const char *colName = psMetadataLookupString(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+	const char *givenCols = psMetadataLookupString(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+											   // columns
+	const char *givenPS = psMetadataLookupString(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+										      // columns
+	
+	// Now, need to get the "given"s
+	if (strlen(givenCols) || strlen(givenPS)) {
+	    psList *cols = papSplit(givenCols, ",;"); // List of column names
+	    psList *values = papSplit(givenPS, ",;"); // List of value names for the columns
+	    psMetadata *selection = psMetadataAlloc(); // The stuff to select in the DB
+	    if (cols->n != values->n) {
+		psLogMsg(__func__, PS_LOG_WARN, "The GIVENDBCOL and GIVENPS entries for %s do not have "
+			 "the same number of entries --- ignored.\n", concept);
+	    } else {
+		// Iterators for the lists
+		psListIterator *colsIter = psListIteratorAlloc(cols, PS_LIST_HEAD, false);
+		psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false);
+		char *column = NULL;	// Name of the column
+		while (column = psListGetAndIncrement(colsIter)) {
+		    char *name = psListGetAndIncrement(valuesIter); // Name for the value
+		    if (!strlen(column) || !strlen(name)) {
+			psLogMsg(__func__, PS_LOG_WARN, "One of the columns or value names for %s is "
+				 " empty --- ignored.\n", concept);
+		    } else {
+			// Search for the value name
+			psMetadataItem *item = p_pmFPAConceptGetFromHeader(fpa, chip, cell, name);
+			if (! item) {
+			    item = p_pmFPAConceptGetFromDefault(fpa, chip, cell, name);
+			}
+			if (! item) {
+			    psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB "
+				     " lookup on %s --- ignored.\n", name, concept);
+			} else {
+			    // We need to create a new psMetadataItem.  I don't think we can't simply hack
+			    // the existing one, since that could conceivably cause memory leaks
+			    psMetadataItem *newItem = psMetadataItemAlloc(concept->name, item->type,
+									  item->comment, item->data.V);
+			    psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+			    psFree(newItem);
+			}
+		    }
+		    psFree(name);
+		    psFree(column);
+		} // Iterating through the columns
+		psFree(colsIter);
+		psFree(valuesIter);
+
+		// Check first to make sure we're only going to touch one row
+		psArray *dbResult = psDBSelectRows(db, tableName, selection, 2); // Lookup result
+		// Note that we use limit=2 in order to test if there are multiple rows returned
+
+		psMetadataItem *result = NULL; // The final result of the DB lookup
+		if (! dbResult || dbResult->n == 0) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- ignored\n", 
+			     concept->name);
+		    return false;
+		} else {
+		    if (dbResult->n > 1) {
+			psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s --- "
+				 " ignored.\n", concept->name);
+		    }
+		    // Update the DB
+		    psMetadata *update = psMetadataAlloc();
+		    psMetadataAddItem(update, concept, PS_LIST_HEAD, 0);
+		    psDBUpdateRows(db, tableName, selection, update);
+		    psFree(update);
+		    return true;
+		}
+	    }
+	    psFree(cols);
+	    psFree(values);
+	}
+    } // Doing the "given"s.
+
+    psAbort(__func__, "Shouldn't ever get here?\n");
+}
+
+
+// Concept set from item
+static bool setConceptItem(pmFPA *fpa, // The FPA
+			   pmChip *chip,// The chip
+			   pmCell *cell,	// The cell
+			   psDB *db, // DB handle
+			   psMetadataItem *concept // Concept item
+    )
+{
+    // Try headers, database, defaults in order
+    psTrace(__func__, 3, "Trying to set concept %s...\n", concept->name);
+    bool status = setConceptInCamera(cell, concept); // Status for return
+    if (! status) {
+	psTrace(__func__, 5, "Trying header....\n");
+	status = setConceptInHeader(fpa, chip, cell, concept);
+    }
+    if (! status) {
+	psTrace(__func__, 5, "Trying database....\n");
+        status = setConceptInDB(fpa, chip, cell, db, concept);
+    }
+    if (! status) {
+	psTrace(__func__, 5, "Checking defaults....\n");
+        status = setConceptInDefault(fpa, chip, cell, concept);
+    }
+
+    if (! status) {
+	psError(PS_ERR_IO, true, "Unable to set %s (%s).\n", concept->name, concept->comment);
+    }
+
+    return status;
+}
+
+
+// Concept set
+static bool setConcept(pmFPA *fpa, // The FPA
+		       pmChip *chip,// The chip
+		       pmCell *cell,	// The cell
+		       psDB *db, // DB handle
+		       psMetadata *concepts, // Concepts MD from which to set
+		       const char *name // Name of the concept
+    )
+{
+    psMetadataItem *concept = psMetadataLookup(concepts, name);
+    if (! concept) {
+	psError(PS_ERR_IO, true, "No such concept as %s\n", name);
+	return false;
+    }
+    return setConceptItem(fpa, chip, cell, db, concept);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Outget concepts from the FPA
+void pmFPAOutgestConcepts(pmFPA *fpa,	// The FPA
+			  psDB *db	// DB handle
+    )
+{
+    // FPA.NAME
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.NAME");
+
+    // FPA.AIRMASS
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.AIRMASS");
+
+    // FPA.FILTER
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.FILTER");
+
+    // FPA.POSANGLE
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.POSANGLE");
+
+    // FPA.RADECSYS
+    setConcept(fpa, NULL, NULL, db, fpa->concepts, "FPA.RADECSYS");
+
+    // These take some extra work
+
+    // FPA.RA
+    {
+	double ra = psMetadataLookupF64(NULL, fpa->concepts, "FPA.RA");	// The RA
+
+	// How to interpret the RA
+	const psMetadata *camera = fpa->camera; // Camera configuration data
+	bool mdok = true;		// Status of MD lookup
+	psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	if (mdok && formats) {
+	    psString raFormat = psMetadataLookupString(&mdok, formats, "FPA.RA");
+	    if (mdok && strlen(raFormat) > 0) {
+		if (strcasecmp(raFormat, "HOURS") == 0) {
+		    ra /= M_PI / 12.0;
+		} else if (strcasecmp(raFormat, "DEGREES") == 0) {
+		    ra /= M_PI / 180.0;
+		} else if (strcasecmp(raFormat, "RADIANS") == 0) {
+		    // No action required
+		} else {
+		    psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.RA in FORMATS (%s) --- assuming"
+			     " HOURS.\n");
+		    ra /= M_PI / 12.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FPA.RA in FORMATS --- assuming HOURS.\n");
+		ra /= M_PI / 12.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+		    "assuming format for FPA.RA is HOURS.\n");
+	    ra /= M_PI / 12.0;
+	}
+
+	// We choose to write sexagesimal format
+	int big, medium;
+	float small;
+	big = (int)ra;
+	medium = (int)(60.0*(ra - (double)big));
+	small = 3600.0*(ra - (double)big - 60.0 * (double)medium);
+	psString raString = psStringCopy("");
+	psStringAppend(&raString, "%d:%d:%.2f", big, medium, small);
+	psMetadataItem *raItem = psMetadataItemAllocStr("FPA.RA", "Right Ascension of the boresight",
+							raString);
+	setConceptItem(fpa, NULL, NULL, db, raItem);
+	psFree(raItem);
+	psFree(raString);
+    }
+
+    // FPA.DEC
+    {
+	double dec = psMetadataLookupF64(NULL, fpa->concepts, "FPA.DEC"); // The DEC
+
+	// How to interpret the DEC
+	const psMetadata *camera = fpa->camera; // Camera configuration data
+	bool mdok = true;		// Status of MD lookup
+	psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	if (mdok && formats) {
+	    psString decFormat = psMetadataLookupString(&mdok, formats, "FPA.DEC");
+	    if (mdok && strlen(decFormat) > 0) {
+		if (strcasecmp(decFormat, "HOURS") == 0) {
+		    dec /= M_PI / 12.0;
+		} else if (strcasecmp(decFormat, "DEGREES") == 0) {
+		    dec /= M_PI / 180.0;
+		} else if (strcasecmp(decFormat, "RADIANS") == 0) {
+		    // No action required
+		} else {
+		    psLogMsg(__func__, PS_LOG_WARN, "Don't understand FPA.DEC in FORMATS (%s) --- assuming"
+			     " DEGREES.\n");
+		    dec /= M_PI / 180.0;
+		}
+	    } else {
+		psError(PS_ERR_IO, false, "Unable to find FPA.DEC in FORMATS --- assuming DEGREES.\n");
+		dec /= M_PI / 180.0;
+	    }
+	} else {
+	    psError(PS_ERR_IO, false, "Unable to find FORMAT metadata in camera configuration --- "
+		    "assuming format for FPA.DEC is HOURS.\n");
+	    dec /= M_PI / 12.0;
+	}
+
+	// We choose to write sexagesimal format
+	int big, medium;
+	float small;
+	big = (int)dec;
+	medium = (int)(60.0*(dec - (double)big));
+	small = 3600.0*(dec - (double)big - 60.0 * (double)medium);
+	psString decString = psStringCopy("");
+	psStringAppend(&decString, "%d:%d:%.2f", big, medium, small);
+	psMetadataItem *decItem = psMetadataItemAllocStr("FPA.DEC", "Right Ascension of the boresight",
+							decString);
+	setConceptItem(fpa, NULL, NULL, db, decItem);
+	psFree(decItem);
+	psFree(decString);
+ 
+    }
+
+    // Pau.
+}
+
+
+// Outgest concepts for the chip
+void pmChipOutgestConcepts(pmChip *chip, // The chip
+			   psDB *db	// DB handle
+    )
+{
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    // CHIP.NAME --- no need to do anything
+
+    // Pau.
+}
+
+
+// Outgest concepts for the cell
+void pmCellOutgestConcepts(pmCell *cell, // The cell
+			   psDB *db	// DB handle
+    )
+{
+    pmChip *chip = cell->parent;	// The parent chip
+    pmFPA *fpa = chip->parent;		// The parent FPA
+
+    // CELL.NAME --- no need to do anything
+
+    // CELL.GAIN
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.GAIN");
+
+    // CELL.READNOISE
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.READNOISE");
+
+    // CELL.SATURATION
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.SATURATION");
+
+    // CELL.BAD
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.BAD");
+
+    // CELL.XPARITY
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.XPARITY");
+
+    // CELL.YPARITY
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.YPARITY");
+
+    // CELL.READDIR
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.READDIR");
+
+    // These used to be pmReadoutGetExposure and pmReadoutGetDarkTime, but that doesn't really make sense at
+    // the moment.  Maybe we need to add a "parent" link to the readouts.  But then how are the exposure times
+    // REALLY derived?  They're not in the FITS headers, because a readout is a plane in a 3D image.  We'll
+    // have to dream up some additional suffix to specify these, but for now....
+
+    // CELL.EXPOSURE (used to be READOUT.EXPOSURE)
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.EXPOSURE");
+
+    // CELL.DARKTIME (used to be READOUT.DARKTIME)
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.DARKTIME");
+
+    // These take some extra work
+
+    // CELL.TRIMSEC
+    {
+	psMetadataItem *trimsecItem = psMetadataLookup(cell->concepts, "CELL.TRIMSEC");
+	psRegion *trimsec = trimsecItem->data.V; // The trimsec region
+	psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.TRIMSEC.SOURCE");
+	if (! sourceItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.TRIMSEC.SOURCE.\n");
+	} else if (sourceItem->type != PS_DATA_STRING) {
+	    psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+	} else {
+	    psString source = sourceItem->data.V; // The source string
+	    
+	    if (strcasecmp(source, "VALUE") == 0) {
+		// Check that it's the same value as stored in the camera
+		psString checkString = psMetadataLookupString(NULL, cell->camera, "CELL.TRIMSEC");
+		psRegion checkRegion = psRegionFromString(checkString);
+		if (! COMPARE_REGIONS(&checkRegion, trimsec)) {
+		    psError(PS_ERR_IO, true, "Target CELL.TRIMSEC is specified by value, and values don't "
+			    "match.\n");
+		}
+	    } else if (strcasecmp(source, "HEADER") == 0) {
+		psString keyword = psMetadataLookupString(NULL, cell->camera, "CELL.TRIMSEC");
+		psMetadata *header = NULL; // The FITS header
+		if (cell->hdu) {
+		    header = cell->hdu->header;
+		} else if (chip->hdu) {
+		    header = chip->hdu->header;
+		} else if (fpa->hdu) {
+		    header = fpa->hdu->header;
+		}
+		if (! header) {
+		    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		} else {
+		    psMetadataAddItem(header, trimsecItem, PS_LIST_TAIL, PS_META_REPLACE);
+		}
+	    } else {
+		psError(PS_ERR_IO, true, "CELL.TRIMSEC.SOURCE (%s) is not HEADER or VALUE.\n", source);
+	    }
+	}
+    }
+
+    // CELL.BIASSEC
+    {
+	psMetadataItem *biassecItem = psMetadataLookup(cell->concepts, "CELL.BIASSEC");
+	psList *biassecs = biassecItem->data.V; // The biassecs region list
+	psMetadataItem *sourceItem = p_pmFPAConceptGet(fpa, chip, cell, db, "CELL.BIASSEC.SOURCE");
+	if (! sourceItem) {
+	    psError(PS_ERR_IO, false, "Couldn't find CELL.BIASSEC.SOURCE.\n");
+	} else if (sourceItem->type != PS_DATA_STRING) {
+	    psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE is not of type STR (%x)\n", sourceItem->type);
+	} else {
+	    psString source = sourceItem->data.V; // The source string
+	    
+	    if (strcasecmp(source, "VALUE") == 0) {
+		// Check that it's the same value as stored in the camera
+		psString checkString = psMetadataLookupString(NULL, cell->camera, "CELL.BIASSEC");
+		psList *checkList = papSplit(checkString, " ;");
+		if (biassecs->n != checkList->n) {
+		    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is specified by value, but number of "
+			    "entries doesn't match.\n");
+		} else {
+		    // We don't care if the order matches or not
+		    psVector *check = psVectorAlloc(biassecs->n, PS_TYPE_U8); // Vector to mark regions off
+		    for (int i = 0; i < check->n; i++) {
+			check->data.U8[i] = 0;
+		    }
+		    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD,
+								       false); // Iterator
+		    psListIterator *checkListIter = psListIteratorAlloc(checkList, PS_LIST_HEAD, false);
+		    psRegion *biassec = NULL; // Region from iteration
+		    while (biassec = psListGetAndIncrement(biassecsIter)) {
+			psListIteratorSet(checkListIter, PS_LIST_HEAD);
+			psString checkRegionString = NULL; // Region string from iteration
+			int i = 0;		// Counter
+			while (checkRegionString = psListGetAndIncrement(checkListIter)) {
+			    psRegion checkRegion = psRegionFromString(checkRegionString);
+			    psTrace(__func__, 7, "Checking [%.0f:%.0f,%.0f:%.0f] against "
+				    "[%.0f:%.0f,%.0f:%.0f]\n", biassec->x0, biassec->x1, biassec->y0,
+				    biassec->y1, checkRegion.x0, checkRegion.x1, checkRegion.y0,
+				    checkRegion.y1);
+			    if (COMPARE_REGIONS(biassec, &checkRegion)) {
+				check->data.U8[i] = 1;
+				i++;
+				break;
+			    }
+			    i++;
+			}
+		    }
+		    for (int i = 0; i < check->n; i++) {
+			if (check->data.U8[i] == 0) {
+			    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is specified by value, but values "
+				    "don't match.\n");
+			    break;
+			}
+		    }
+		    psFree(checkListIter);
+		    psFree(biassecsIter);
+		    psFree(check);
+		}
+		psFree(checkList);
+	    } else if (strcasecmp(source, "HEADER") == 0) {
+		psString keywordsString = psMetadataLookupString(NULL, cell->camera, "CELL.BIASSEC");
+		psList *keywords = papSplit(keywordsString, " ,;");
+		if (biassecs->n != keywords->n) {
+		    psError(PS_ERR_IO, true, "Target CELL.BIASSEC is sepcified by headers, but the number "
+			    "of headers doesn't match.\n");
+		} else {
+		    psMetadata *header = NULL; // The FITS header
+		    if (cell->hdu) {
+			header = cell->hdu->header;
+		    } else if (chip->hdu) {
+			header = chip->hdu->header;
+		    } else if (fpa->hdu) {
+			header = fpa->hdu->header;
+		    }
+		    if (! header) {
+			psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		    } else {
+			psListIterator *keywordsIter = psListIteratorAlloc(keywords, PS_LIST_HEAD, false);
+			psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false);
+			psString keyword = NULL; // Header keyword from list
+			while (keyword = psListGetAndIncrement(keywordsIter)) {
+			    // Update the header
+			    psRegion *biassec = psListGetAndIncrement(biassecsIter);
+			    psString biassecString = psRegionToString(*biassec);
+			    psMetadataAdd(header, PS_LIST_TAIL, keyword, PS_DATA_STRING | PS_META_REPLACE,
+					  "Bias section", biassecString);
+			    psFree(biassecString);
+			}
+			psFree(keywordsIter);
+			psFree(biassecsIter);
+		    }
+		}
+		psFree(keywords);
+	    } else {
+		psError(PS_ERR_IO, true, "CELL.BIASSEC.SOURCE (%s) is not HEADER or VALUE.\n", source);
+	    }
+	}
+    }
+
+    // CELL.XBIN and CELL.YBIN
+    {
+	psMetadataItem *xBinItem = psMetadataLookup(cell->concepts, "CELL.XBIN"); // Binning factor in x
+	psMetadataItem *yBinItem = psMetadataLookup(cell->concepts, "CELL.YBIN"); // Binning factor in y
+
+	// If these are specified by the header, we need to check to see if it's the same header keyword
+	const psMetadata *camera = fpa->camera;
+	psMetadata *translation = psMetadataLookupMD(NULL, camera, "TRANSLATION");
+	psString xKeyword = psMetadataLookupString(NULL, translation, "CELL.XBIN");
+	psString yKeyword = psMetadataLookupString(NULL, translation, "CELL.YBIN");
+	if (strlen(xKeyword) > 0 && strcasecmp(xKeyword, yKeyword) != 0) {
+	    setConceptInHeader(fpa, chip, cell, xBinItem);
+	    xBinItem = NULL;
+	}
+	if (strlen(yKeyword) > 0 && strcasecmp(xKeyword, yKeyword) != 0) {
+	    setConceptInHeader(fpa, chip, cell, yBinItem);
+	    yBinItem = NULL;
+	}
+	if (strlen(xKeyword) > 0 && strlen(yKeyword) > 0 && strcasecmp(xKeyword, yKeyword) == 0) {
+	    psString binString = psStringCopy("");
+	    psStringAppend(&binString, "%d,%d", xBinItem->data.S32, yBinItem->data.S32);
+	    psMetadataItem *binItem = psMetadataItemAllocStr(xKeyword, "Binning factor in x and y",
+							     binString);
+	    psFree(binString);
+	    psMetadata *header = NULL; // The FITS header
+	    if (cell->hdu) {
+		header = cell->hdu->header;
+	    } else if (chip->hdu) {
+		header = chip->hdu->header;
+	    } else if (fpa->hdu) {
+		header = fpa->hdu->header;
+	    }
+	    if (! header) {
+		psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+	    } else {
+		psMetadataAddItem(header, binItem, PS_LIST_TAIL, PS_META_REPLACE);
+		xBinItem = NULL;
+		yBinItem = NULL;
+	    }
+	    psFree(binItem);
+	}
+
+	// Do it in the usual way if we have to
+	if (xBinItem) {
+	    setConceptItem(fpa, chip, cell, db, xBinItem);
+	}
+	if (yBinItem) {
+	    setConceptItem(fpa, chip, cell, db, yBinItem);
+	}
+    }
+
+    // CELL.TIMESYS
+    {
+	psMetadataItem *sysItem = psMetadataLookup(cell->concepts, "CELL.TIMESYS");
+	psString sys = NULL;		// String to store
+	switch (sysItem->data.S32) {
+	  case PS_TIME_TAI:
+	    sys = psStringCopy("TAI");
+	    break;
+	  case PS_TIME_UTC:
+	    sys = psStringCopy("UTC");
+	    break;
+	  case PS_TIME_UT1:
+	    sys = psStringCopy("UT1");
+	    break;
+	  case PS_TIME_TT:
+	    sys = psStringCopy("TT");
+	    break;
+	  default:
+	    sys = psStringCopy("Unknown");
+	}
+	psMetadataItem *newItem = psMetadataItemAllocStr("CELL.TIMESYS", "Time system", sys);
+	setConceptItem(fpa, chip, cell, db, newItem);
+	psFree(newItem);
+	psFree(sys);
+    }
+
+    // CELL.TIME
+    {
+	psMetadataItem *timeItem = psMetadataLookup(cell->concepts, "CELL.TIME");
+	psTime *time = timeItem->data.V; // The time
+	psString dateTimeString = psTimeToISO(time); // String representation
+
+	psMetadata *formats = psMetadataLookupMD(NULL, fpa->camera, "FORMATS");
+	psString format = psMetadataLookupString(NULL, formats, "CELL.TIME");
+
+	if (strlen(format) == 0) {
+	    // No format specified --> do it in the usual way (maybe it's a DB lookup)
+	    psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								 dateTimeString);
+	    setConceptItem(fpa, chip, cell, db, newTimeItem);
+	    psFree(newTimeItem);
+	} else {
+	    if (strcasecmp(format, "ISO") == 0) {
+		// dateTimeString contains an ISO time
+		psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								     dateTimeString);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else if (strstr(format, "SEPARATE")) {
+		// We're working with two separate headers
+		psMetadata *header = NULL; // The FITS header
+		if (cell->hdu) {
+		    header = cell->hdu->header;
+		} else if (chip->hdu) {
+		    header = chip->hdu->header;
+		} else if (fpa->hdu) {
+		    header = fpa->hdu->header;
+		}
+		if (! header) {
+		    psError(PS_ERR_IO, true, "Unable to find FITS header!\n");
+		} else {
+		    // Get the headers
+		    const psMetadata *camera = fpa->camera;
+		    psMetadata *translation = psMetadataLookupMD(NULL, camera, "TRANSLATION");
+		    psString keywords = psMetadataLookupString(NULL, translation, "CELL.TIME");
+		    psList *dateTimeKeywords = papSplit(keywords, " ,;");
+		    psString dateKeyword = psListGet(dateTimeKeywords, PS_LIST_HEAD);
+		    psString timeKeyword = psListGet(dateTimeKeywords, PS_LIST_TAIL);
+
+		    psList *dateTime = papSplit(dateTimeString, " T"); // Find the middle T
+		    psString dateString = psListGet(dateTime, PS_LIST_HEAD);
+		    psString timeString = psListGet(dateTime, PS_LIST_TAIL);
+
+		    // XXX: Couldn't be bothered doing these right now
+		    if (strstr(format, "PRE2000")) {
+			psError(PS_ERR_IO, true, "Don't you realise it's the twenty-first century?\n");
+		    }
+		    if (strstr(format, "BACKWARDS")) {
+			psError(PS_ERR_IO, true, "You want it BACKWARDS?  Not right now, thanks.\n");
+		    }
+
+		    psMetadataItem *dateItem = psMetadataItemAllocStr(dateKeyword, "Date of observation",
+								      dateString);
+		    psMetadataItem *timeItem = psMetadataItemAllocStr(timeKeyword, "Time of observation",
+								      timeString);
+		    psMetadataAddItem(header, dateItem, PS_LIST_TAIL, PS_META_REPLACE);
+		    psMetadataAddItem(header, timeItem, PS_LIST_TAIL, PS_META_REPLACE);
+
+		    psFree(dateTimeKeywords);
+		    psFree(dateTime);
+		    psFree(dateItem);
+		    psFree(timeItem);
+		}
+	    } else if (strcasecmp(format, "MJD") == 0) {
+		double mjd = psTimeToMJD(time);
+		psMetadataItem *newTimeItem = psMetadataItemAllocF64("CELL.TIME", "MJD of observation", mjd);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else if (strcasecmp(format, "JD") == 0) {
+		double jd = psTimeToMJD(time);
+		psMetadataItem *newTimeItem = psMetadataItemAllocF64("CELL.TIME", "JD of observation", jd);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    } else {
+		psError(PS_ERR_IO, true, "Not sure how to outgest CELL.TIME (%s) --- trying "
+			"ISO\n", dateTimeString);
+		psMetadataItem *newTimeItem = psMetadataItemAllocStr("CELL.TIME", "Time of observation",
+								     dateTimeString);
+		setConceptItem(fpa, chip, cell, db, newTimeItem);
+		psFree(newTimeItem);
+	    }
+	}
+
+	psFree(dateTimeString);
+    }
+
+    // Remove corrective
+    {
+	const psMetadata *camera = fpa->camera;
+	bool mdok = false;		// Result of MD lookup
+	psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS");
+	if (mdok && formats) {
+	    psString format = psMetadataLookupString(&mdok, formats, "CELL.X0");
+	    if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
+		psMetadataItem *cellx0 = psMetadataLookup(cell->concepts, "CELL.X0");
+		cellx0->data.S32 += 1;
+	    }
+	    format = psMetadataLookupString(&mdok, formats, "CELL.Y0");
+	    if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
+		psMetadataItem *celly0 = psMetadataLookup(cell->concepts, "CELL.Y0");
+		celly0->data.S32 += 1;
+	    }
+	}
+    }
+    // CELL.X0
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.X0");
+    // CELL.Y0
+    setConcept(fpa, chip, cell, db, cell->concepts, "CELL.Y0");
+
+    // Pau.
+}
+
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConceptsSet.h	(revision 7462)
@@ -0,0 +1,17 @@
+#ifndef PM_FPA_CONCEPTS_SET_H
+#define PM_FPA_CONCEPTS_SET_H
+
+void pmFPAOutgestConcepts(pmFPA *fpa,	// The FPA
+			  psDB *db	// DB handle
+    );
+void pmChipOutgestConcepts(pmChip *chip,	// The chip
+			   psDB *db	// DB handle
+    );
+void pmCellOutgestConcepts(pmCell *cell, // The cell
+			   psDB *db	// DB handle
+    );
+
+
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.c	(revision 7462)
@@ -0,0 +1,320 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPAConstruct.h"
+
+#include "papStuff.h"
+
+// NOTE: Need to deal with header inheritance
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Read data for a particular cell from the camera configuration
+static psMetadata *getCellData(const psMetadata *camera, // The camera configuration
+			       const char *cellName // The name of the cell
+    )
+{
+    bool status = true;			// Result of MD lookup
+    psMetadata *cells = psMetadataLookupMD(&status, camera, "CELLS"); // The CELLS
+    if (! status) {
+	psError(PS_ERR_IO, false, "Unable to determine CELLS of camera.\n");
+	return NULL;
+    }
+
+    psMetadata *cellData = psMetadataLookupMD(&status, cells, cellName); // The data for the particular cell
+    if (! status) {
+	psError(PS_ERR_IO, false, "Unable to find specs for cell %s: ignored\n", cellName);
+    }
+
+#if 0
+    // Need to create a new instance, so that each cell can work with its own
+    psMetadata *copy = psMetadataAlloc();
+    psMetadataIterator *iter = psMetadataIteratorAlloc(cellData, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *item = NULL;	// Item from iteration
+    while (item = psMetadataGetAndIncrement(iter)) {
+	if (item->type == PS_DATA_METADATA_MULTI || item->type == PS_DATA_METADATA) {
+	    psLogMsg(__func__, PS_LOG_WARN, "PS_DATA_METADATA_MULTI and PS_DATA_METADATA are not supported "
+		     "in a cell definition --- %s ignored.\n", item->name);
+	    continue;
+	}
+	if (! psMetadataAdd(copy, PS_LIST_TAIL, item->name, item->type, item->comment, item->data.V)) {
+	    psAbort(__func__, "Should never reach here!\n");
+	}
+    }
+    psFree(iter);
+
+    return copy;
+#else
+    return cellData;
+#endif
+
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmFPA *pmFPAConstruct(const psMetadata *camera // The camera configuration
+    )
+{
+    pmFPA *fpa = pmFPAAlloc(camera); // The FPA to fill out
+    bool mdStatus = true;		// Status from metadata lookups
+    const char *phuType = psMetadataLookupString(&mdStatus, camera, "PHU"); // What is the PHU?
+    const char *extType = psMetadataLookupString(&mdStatus, camera, "EXTENSIONS"); // What's in the extns?
+
+    if (strcasecmp(phuType, "FPA") == 0) {
+	// The FITS file contains an entire FPA
+
+	psMetadata *contents = psMetadataLookupMD(&mdStatus, camera, "CONTENTS"); // The CONTENTS
+	if (! mdStatus) {
+	    psError(PS_ERR_IO, false, "Unable to determine CONTENTS of camera.\n");
+	    psFree(fpa);
+	    return NULL;
+	}
+
+	// Set up iteration over the contents
+	psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL);
+	psMetadataItem *contentItem = NULL; // Item from the metadata
+
+	if (strcasecmp(extType, "CHIP") == 0) {
+	    // Extensions are chips; Content contains a list of cells
+	    while (contentItem = psMetadataGetAndIncrement(contentsIter)) {
+		psString extName = contentItem->name; // The name of the extension
+		pmChip *chip = pmChipAlloc(fpa, extName); // The chip
+		chip->hdu = p_pmHDUAlloc(extName); // Prepare chip to receive FITS data
+		if (contentItem->type != PS_DATA_STRING) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Type of content item (%x) is not string: ignored\n",
+			     contentItem->type);
+		    continue;
+		}
+
+		const char *content = contentItem->data.V; // The content of the extension
+		psTrace(__func__, 7, "Content of %s is: %s\n", extName, content);
+		psList *cellNames = papSplit(content, " ,"); // A list of the component cells
+		psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, NULL);
+		char *cellName = NULL; // The name of a cell
+		while (cellName = psListGetAndIncrement(cellNamesIter)) {
+		    // Get the cell data
+		    psMetadata *cellData = getCellData(camera, cellName);
+		    pmCell *cell = pmCellAlloc(chip, cellData, cellName); // The cell
+//		    psFree(cellData);
+		}
+		psFree(cellNamesIter);
+		psFree(cellNames);
+	    }
+
+	} else if (strcasecmp(extType, "CELL") == 0) {
+	    // Extensions are cells; Content contains a chip name and cell type
+	    psMetadata *chips = psMetadataAlloc(); // Given a chip name, holds the chip number
+	    while (contentItem = psMetadataGetAndIncrement(contentsIter)) {
+		psString extName = contentItem->name; // The name of the extension
+		psTrace(__func__, 1, "Getting %s....\n", extName);
+
+		if (contentItem->type != PS_DATA_STRING) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Type of content item (%x) is not string: ignored\n",
+			     contentItem->type);
+		    continue;
+		}
+		const char *content = contentItem->data.V; // The content of the extension
+		psList *contents = papSplit(content, ": "); // Split the name from the type
+		if (contents->n != 2) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Unable to read contents of %s: ignored.\n", extName);
+		} else {
+		    psString chipName = psListGet(contents, 0); // The name of the chip
+		    psString cellType = psListGet(contents, 1); // The type of cell
+		    psTrace(__func__, 7, "Extension is cell of type %s, from chip %s\n", cellType,
+			    chipName);
+		    
+		    psMetadataItem *chipItem = psMetadataLookup(chips, chipName); // Item containing the chip
+		    pmChip *chip = NULL; // The chip
+		    if (! chipItem) {
+			chip = pmChipAlloc(fpa, chipName);
+			psMetadataAdd(chips, PS_LIST_TAIL, chipName, PS_DATA_UNKNOWN, "", chip);
+		    } else {
+			chip = psMemIncrRefCounter(chipItem->data.V);
+		    }
+		    // The cell
+		    psMetadata *cellData = getCellData(camera, cellType);
+		    pmCell *cell = pmCellAlloc(chip, cellData, extName); // The cell
+//		    psFree(cellData);
+		    cell->hdu = p_pmHDUAlloc(extName); // Prepare cell to receive FITS data
+
+		    psFree(chip);
+		    psFree(cell);
+		}
+		psFree(contents);
+	    }
+	    psFree(chips);
+
+	} else if (strcasecmp(extType, "NONE") == 0) {
+	    // No extensions; Content contains metadata, each entry is a chip with its component cells
+	    fpa->hdu = p_pmHDUAlloc("PHU"); // Prepare FPA to receive FITS data
+	    while (contentItem = psMetadataGetAndIncrement(contentsIter)) {
+		psString chipName = contentItem->name; // The name of the chip
+
+		if (contentItem->type != PS_DATA_STRING) {
+		    psLogMsg(__func__, PS_LOG_WARN, "Type of content item (%x) is not string: ignored\n",
+			     contentItem->type);
+		    continue;
+		}
+		psString content = contentItem->data.V; // The content of the extension
+		psTrace(__func__, 5, "Component cells are: %s\n", content);
+		pmChip *chip = pmChipAlloc(fpa, chipName); // The chip
+		psList *cellNames = papSplit(content, ", "); // Split the list of cells
+		psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false);
+		char *cellName = NULL; // Name of the cell
+		while (cellName = psListGetAndIncrement(cellNamesIter)) {
+		    psTrace(__func__, 7, "Processing cell %s....\n", cellName);
+		    psMetadata *cellData = getCellData(camera, cellName);
+		    pmCell *cell = pmCellAlloc(chip, cellData, cellName); // The cell
+//		    psFree(cellData);
+		}
+		psFree(cellNamesIter);
+	    }
+
+	} else {
+	    psError(PS_ERR_IO, false, "EXTENSIONS in camera definition is not CHIP, CELL or NONE.\n");
+	    psFree(fpa);
+	    return NULL;
+	} // Type of extension
+
+	psFree(contentsIter);
+
+    } else if (strcasecmp(phuType, "CHIP") == 0) {
+	// The FITS file contains a single chip only
+	psString chipName = psStringCopy("CHIP"); // Name for chip
+	pmChip *chip = pmChipAlloc(fpa, chipName); // The chip
+
+	if (strcasecmp(extType, "NONE") == 0) {
+	    // There are no extensions --- only the PHU
+	    chip->hdu = p_pmHDUAlloc("PHU");
+
+	    const char *contents = psMetadataLookupString(&mdStatus, camera, "CONTENTS");
+	    if (! mdStatus) {
+		psError(PS_ERR_IO, false, "Unable to determine CONTENTS of camera.\n");
+		psFree(fpa);
+		return NULL;
+	    }
+	    psList *cellNames = papSplit(contents, " ,"); // Names of cells
+	    psListIterator *cellIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false); // Iterator
+	    psString cellName = NULL;
+	    while (cellName = psListGetAndIncrement(cellIter)) {
+		psMetadata *cellData = getCellData(camera, cellName);
+		pmCell *cell = pmCellAlloc(chip, cellData, cellName); // The cell
+//		psFree(cellData);
+	    }
+	    psFree(cellIter);
+
+	} else if (strcasecmp(extType, "CELL") == 0) {
+	    // Extensions are cells
+	    psMetadata *contents = psMetadataLookupMD(&mdStatus, camera, "CONTENTS"); // The CONTENTS
+	    if (! mdStatus) {
+		psError(PS_ERR_IO, false, "Unable to determine CONTENTS of camera.\n");
+		psFree(fpa);
+		return NULL;
+	    }
+	    
+	    if (strcasecmp(extType, "CELL") != 0) {
+		psLogMsg(__func__, PS_LOG_WARN, "EXTENSIONS in camera definition is %s, but PHU is CHIP.\n"
+			 "EXTENSIONS assumed to be CELL.\n", extType);
+	    }
+
+	    // Iterate through the contents
+	    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL);
+	    psMetadataItem *contentItem = NULL; // Item from metadata
+	    while (contentItem = psMetadataGetAndIncrement(contentsIter)) {
+		psString extName = contentItem->name; // The name of the extension
+		// Content is a cell type
+		if (contentItem->type != PS_DATA_STRING) {
+		    psLogMsg(__func__, PS_LOG_WARN,
+			     "CONTENT metadata for extension %s is not of type string, but %x --- ignored\n",
+			     extName, contentItem->type);
+		    continue;
+		}
+		const char *cellType = contentItem->data.V; // The type of cell
+		psTrace(__func__, 5, "Cell type is %s\n", cellType);
+		psMetadata *cellData = getCellData(camera, cellType);
+		pmCell *cell = pmCellAlloc(chip, cellData, extName); // The cell
+//		psFree(cellData);
+		cell->hdu = p_pmHDUAlloc(extName); // Prepare cell to receive FITS data
+
+		psFree(cell);
+	    } // Iterating through contents
+	    psFree(contentsIter);
+
+	} else {
+	    psError(PS_ERR_IO, false, "EXTENSIONS in camera definition is neither CELL or NONE.\n");
+	    psFree(fpa);
+	    return NULL;
+	}
+	psFree(chipName);
+	psFree(chip);
+
+    } else {
+	psError(PS_ERR_IO, true,
+		"The PHU type specified in the camera configuration (%s) is not FPA or CHIP.\n",
+		phuType);
+	psFree(fpa);
+	return NULL;
+    }
+
+    return fpa;
+}
+
+// Print out the focal plane structure
+void pmFPAPrint(pmFPA *fpa		// FPA to print
+    )
+{
+    psTrace(__func__, 1, "FPA:\n");
+    if (fpa->hdu) {
+	psTrace(__func__, 2, "---> FPA is extension %s.\n", fpa->hdu->extname);
+	if (! fpa->hdu->images) {
+	    psTrace(__func__, 2, "---> NO PIXELS for extension %s\n", fpa->hdu->extname);
+	}
+    }
+    psMetadataPrint(fpa->concepts, 2);
+
+    psArray *chips = fpa->chips;	// Array of chips
+    // Iterate over the FPA
+    for (int i = 0; i < chips->n; i++) {
+	psTrace(__func__, 3, "Chip: %d\n", i);
+	pmChip *chip = chips->data[i]; // The chip
+	if (chip->hdu) {
+	    psTrace(__func__, 4, "---> Chip is extension %s.\n", chip->hdu->extname);
+	    if (! chip->hdu->images) {
+		psTrace(__func__, 4, "---> NO PIXELS for extension %s\n", chip->hdu->extname);
+	    }
+	}
+	psMetadataPrint(chip->concepts, 4);
+
+	// Iterate over the chip
+	psArray *cells = chip->cells;	// Array of cells
+	for (int j = 0; j < cells->n; j++) {
+	    psTrace(__func__, 5, "Cell: %d\n", j);
+	    pmCell *cell = cells->data[j]; // The cell
+	    if (cell->hdu) {
+		psTrace(__func__, 6, "---> Cell is extension %s.\n", cell->hdu->extname);
+		if (! cell->hdu->images) {
+		    psTrace(__func__, 6, "---> NO PIXELS for extension %s\n", cell->hdu->extname);
+		}
+	    }
+	    psMetadataPrint(cell->concepts, 6);
+
+	    psTrace(__func__, 7, "Readouts:\n");
+	    psArray *readouts = cell->readouts;	// Array of readouts
+	    for (int k = 0; k < readouts->n; k++) {
+		pmReadout *readout = readouts->data[k]; // The readout
+		psImage *image = readout->image; // The image
+		psTrace(__func__, 8, "Image: [%d:%d,%d:%d] (%dx%d)\n", image->col0, image->col0 + 
+			image->numCols, image->row0, image->row0 + image->numRows, image->numCols,
+			image->numRows);
+	    } // Iterating over cell
+	} // Iterating over chip
+    } // Iterating over FPA
+
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAConstruct.h	(revision 7462)
@@ -0,0 +1,16 @@
+#ifndef PM_FPA_CONSTRUCT_H
+#define PM_FPA_CONSTRUCT_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+// Read the contents of a FITS file (format specified by the camera configuration) into memory
+pmFPA *pmFPAConstruct(const psMetadata *camera // The camera configuration
+    );
+
+// Print out the FPA
+void pmFPAPrint(pmFPA *fpa		// FPA to print
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.c	(revision 7462)
@@ -0,0 +1,727 @@
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include "pslib.h"
+
+#include "papStuff.h"
+#include "pmFPA.h"
+#include "pmFPAMorph.h"
+
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+
+
+// Flip the chip in x
+static psImage *xFlipImage(psImage *image, // Image to be flipped
+                           int size     // Size of image in flip dimension
+    )
+{
+    psImage *flipped = psImageAlloc(size, image->numRows, image->type.type); // Flipped image
+    switch (image->type.type) {
+      case PS_TYPE_U16:
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                flipped->data.U16[y][size - x - 1] = image->data.U16[y][x];
+            }
+        }
+        break;
+      case PS_TYPE_F32:
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                flipped->data.F32[y][size - x - 1] = image->data.F32[y][x];
+            }
+        }
+        break;
+      default:
+        psError(PS_ERR_IO, true, "Image type %x not yet supported for flip.\n", image->type.type);
+    }
+
+    return flipped;
+}
+
+// Flip the chip in y
+static psImage *yFlipImage(psImage *image, // Image to be flipped
+                           int size     // Size of image in flip dimension
+    )
+{
+    psImage *flipped = psImageAlloc(image->numCols, size, image->type.type); // Flipped image
+    switch (image->type.type) {
+      case PS_TYPE_U16:
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                flipped->data.U16[size - y - 1][x] = image->data.U16[y][x];
+            }
+        }
+        break;
+      default:
+        psError(PS_ERR_IO, true, "Image type %x not yet supported for flip.\n", image->type.type);
+    }
+
+    return flipped;
+}
+
+
+#if 0
+// Return a list of the region strings and the method for interpreting them
+static psList *getRegions(psString *method, // Method for evaluating the regions
+                          psString methodValues // The METHOD:VALUES string
+    )
+{
+    char *values = strchr(methodValues, ':'); // The VALUES
+    *method = psStringNCopy(methodValues, strlen(methodValues) - strlen(values)); // The METHOD
+    psList *regionStrings = NULL;       // List of the region strings
+    if (strncmp(*method, "HEADER", 6) == 0 || strncmp(*method, "HD", 2) == 0) {
+        regionStrings = papSplit(values, ", ");
+    } else if (strncmp(*method, "VALUE", 5) == 0 || strncmp(*method, "VAL", 3) == 0) {
+        regionStrings = papSplit(values, "; ");
+    }
+
+    return regionStrings;
+}
+#endif
+
+
+// Splice an image into another image
+static psRegion *spliceImage(psImage *target, // Target image onto which to splice
+                             int *x0, int *y0, // Starting coordinates to splice to
+                             psImage *source, // Source image from which to splice
+                             const psRegion *from, // Region to splice from
+                             int readdir, // Read direction
+                             bool xFlip, bool yFlip // Flip x and y axes?
+                        )
+{
+    psImage *toSplice = psImageSubset(source, *from); // Subimage, to splice into target
+    if (xFlip) {
+        int size = 0;           // Size of flipped image
+        if (readdir == 1) {
+            size = toSplice->numCols;
+        } else if (readdir == 2) {
+            size = target->numCols;
+        }
+        psImage *temp = xFlipImage(toSplice, size);
+        psFree(toSplice);
+        toSplice = temp;
+    }
+    if (yFlip) {
+        int size = 0;           // Size of flipped image
+        if (readdir == 1) {
+            size = target->numRows;
+        } else if (readdir == 2) {
+            size = toSplice->numRows;
+        }
+        psImage *temp = yFlipImage(toSplice, size);
+        psFree(toSplice);
+        toSplice = temp;
+    }
+
+    psTrace(__func__, 5, "Splicing %dx%d image in at %d,%d\n", toSplice->numCols, toSplice->numRows,
+            *x0, *y0);
+    (void)psImageOverlaySection(target, toSplice, *x0, *y0, "=");
+
+    if (readdir == 1) {
+        *x0 += toSplice->numCols;
+    } else if (readdir == 2) {
+        *y0 += toSplice->numRows;
+    }
+
+    // Return the region where we pasted the image
+    psRegion *outRegion = psAlloc(sizeof(psRegion));
+    // Addding 1 to get FITS standard
+    outRegion->x0 = *x0 + 1;
+    outRegion->x1 = *x0 + toSplice->numCols;
+    outRegion->y0 = *y0 + 1;
+    outRegion->y1 = *y0 + toSplice->numRows;
+
+    psFree(toSplice);
+    return outRegion;
+}
+
+// Splice bias regions (overscans)
+static bool spliceBias(psImage *splice, // Image to which to splice
+                       int *x0, int *y0, // Starting position for splice
+                       const pmCell *in, // Input cell
+                       pmCell *out,     // Output cell
+                       int xFlip, int yFlip, // Flip the image?
+                       int readNum,     // Number of readout
+                       int readdir      // Read direction
+                       )
+{
+    psArray *readouts = in->readouts; // The readouts;
+    pmReadout *readout = readouts->data[readNum]; // The readout of interest
+    psList *inBiassecs = psMetadataLookupPtr(NULL, in->concepts, "CELL.BIASSEC"); // List of input biassecs
+    psListIterator *inBiassecsIter = psListIteratorAlloc(inBiassecs, PS_LIST_HEAD, false);
+    psRegion *inBiassec = NULL; // BIASSEC from list
+    psList *outBiassecs = psListAlloc(NULL); // List of biassecs for output
+    while (inBiassec = psListGetAndIncrement(inBiassecsIter)) {
+        psRegion *outBiassec = spliceImage(splice, x0, y0, readout->image, inBiassec, readdir, xFlip,
+                                           yFlip);
+        psListAdd(outBiassecs, PS_LIST_TAIL, outBiassec);
+    }
+    psFree(inBiassecsIter);
+    psMetadataAdd(out->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_DATA_LIST | PS_META_REPLACE,
+                  "BIASSEC from pmFPAMorph", outBiassecs);
+
+    return true;
+}
+
+psImage *p_pmImageMosaic(const psArray *source, // Images to splice in
+                         const psVector *xFlip, const psVector *yFlip, // Need to flip x and y?
+                         const psVector *xBinSource, const psVector *yBinSource, // Binning in x and y of
+                                                                                 // source images
+                         int xBinTarget, int yBinTarget, // Binning in x and y of target images
+                         const psVector *x0, const psVector *y0 // Offsets for source images on target
+                         );
+
+// Splice a list of cells together
+// We need to do the following:
+// 1. Copy the contents of the cells (i.e., the readouts) over from the in to the out
+// 2. Splice together everything in the bucket for the image that we will write out
+static psArray *spliceCells(psList *outCells, // List of target cells (required for parity info)
+                            psList *inCells, // List of cells to splice together
+                            bool posDep // Position dependent placement of overscans?
+    )
+{
+    assert(outCells);
+    assert(inCells);
+
+    psTrace(__func__, 5, "Splicing together all cells in the bucket.\n");
+
+    if (inCells->n != outCells->n) {
+        psError(PS_ERR_IO, true, "Sizes of input (%d) and output (%d) lists of cells don't match.\n",
+                inCells->n, outCells->n);
+        return NULL;
+    }
+
+    int numCells = inCells->n;          // Number of cells
+    int readdir = 0;                    // Read direction for CCD; currently unknown
+    int numReadouts = 0;                // Number of readouts in a cell
+    psArray *spliced = NULL;            // Array of spliced readouts
+    psElemType type = 0;                // Image type (U16, S32, F32, etc)
+
+    psVector *xFlip = psVectorAlloc(numCells, PS_TYPE_U8); // Do we need to flip in x?
+    psVector *yFlip = psVectorAlloc(numCells, PS_TYPE_U8); // Do we need to flip in y?
+    psVector *xBin = psVectorAlloc(numCells, PS_TYPE_S32); // Binning in x
+    psVector *yBin = psVectorAlloc(numCells, PS_TYPE_S32); // Binning in y
+    psVector *x0 = psVectorAlloc(numCells, PS_TYPE_S32); // Offset in x
+    psVector *y0 = psVectorAlloc(numCells, PS_TYPE_S32); // Offset in y
+    int xBinOut = 0;                    // Binning in x for output
+    int yBinOut = 0;                    // Binning in y for output
+
+    // We go through the cells to make sure everything's kosher, and to get the sizes.
+    psListIterator *inCellsIter = psListIteratorAlloc(inCells, PS_LIST_HEAD, false); // Iterator for cells
+    psListIterator *outCellsIter = psListIteratorAlloc(outCells, PS_LIST_HEAD, false); // Iterator for cells
+    pmCell *inCell = NULL;              // Cell from list of input cells
+    pmCell *outCell = NULL;             // Cell from list of output cells
+    int cellNum = -1;                   // Cell number
+    while (inCell = psListGetAndIncrement(inCellsIter) && outCell = psListGetAndIncrement(outCellsIter)) {
+        cellNum++;
+        psArray *inReadouts = inCell->readouts;   // The readouts comprising the cell
+
+        // Get and check the read direction
+        int cellReaddir = psMetadataLookupS32(NULL, inCell->concepts, "CELL.READDIR");
+        psTrace(__func__, 10, "Checking read direction for cell %d: %d\n", cellNum, cellReaddir);
+        if (cellNum == 0) {
+            // First run through; set it.
+            readdir = cellReaddir;
+        } else if (readdir != cellReaddir) {
+            psError(PS_ERR_IO, true, "Trying to splice cells with different read directions (%d vs %d). I "
+                    "don't know how to do that!\n", readdir, cellReaddir);
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        // Get and check the number of readouts
+        if (! spliced) {
+            numReadouts = inReadouts->n;
+            spliced = psArrayAlloc(numReadouts);
+            xSize = psVectorAlloc(numReadouts, PS_TYPE_S32);
+            ySize = psVectorAlloc(numReadouts, PS_TYPE_S32);
+            for (int i = 0; i < numReadouts; i++) {
+                xSize->data.S32[i] = 0;
+                ySize->data.S32[i] = 0;
+            }
+        } else if (inReadouts->n != numReadouts) {
+            psError(PS_ERR_IO, true, "Trying to splice cells with different number of reads (%d vs %d)\n",
+                    numReadouts, readouts->n);
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        // Get and check the binning --- we can't splice to cells that have different binnings, because
+        // the spliced image can't be defined.  Hence all the "to" cells must have the same binning.
+        xBinIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.XBIN");
+        yBinIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.YBIN");
+        int xBinCheck = psMetadataLookupS32(NULL, outCell->concepts, "CELL.XBIN");
+        int yBinCheck = psMetadataLookupS32(NULL, outCell->concepts, "CELL.YBIN");
+        if (xBinOut == 0) {
+            xBinOut = xBinCheck;
+        } else if (xBinCheck != xBinOut) {
+            psError(PS_ERR_IO, true, "Trying to splice to cells with different x binnings (%d vs %d)\n",
+                    xBinOut, xBinCheck);
+            // XXX Clean up before returning
+            return NULL;
+        }
+        if (yBinOut == 0) {
+            yBinOut = yBinCheck;
+        } else if (yBinCheck != yBinOut) {
+            psError(PS_ERR_IO, true, "Trying to splice to cells with different y binnings (%d vs %d)\n",
+                    yBinOut, yBinCheck);
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        // Copy the readouts over
+        psArray *outReadouts = psArrayAlloc(inReadouts->n); // Array of readouts for the output cell
+        for (int i = 0; i < inReadouts->n; i++) {
+            outReadouts->data[i] = psMemIncrRefCounter(inReadouts->data[i]);
+        }
+
+        // Get the offsets
+        x0->data.S32[i] = psMetadataLookupS32(NULL, inCell->concepts, "CELL.X0");
+        y0->data.S32[i] = psMetadataLookupS32(NULL, inCell->concepts, "CELL.Y0");
+
+        // Get and check the parity
+        int xParityIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.XPARITY");
+        int yParityIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.YPARITY");
+        int xParityOut = psMetadataLookupS32(NULL, outCell->concepts, "CELL.XPARITY");
+        int yParityOut = psMetadataLookupS32(NULL, outCell->concepts, "CELL.YPARITY");
+        if (xParityIn == 0 || xParityOut == 0 || yParityIn == 0 || yParityOut == 0) {
+            psError(PS_ERR_IO, false, "Unable to determine parity of cell!\n");
+            // XXX Clean up before returning
+            return NULL;
+        }
+        if (xParityIn != xParityOut) {
+            for (int i = 0; i < outReadouts->n; i++) {
+                psImage *image = outReadouts->data[i];
+                outReadouts->data[i] = xFlipImage(image);
+                psFree(image);
+            }
+            // Correct CELL.X0 for the parity shift
+            psMetadataItem *x0item = psMetadataLookup(inCell->concepts, "CELL.X0");
+            x0item->data.S32 -= xSize->data.S32[cellNum];
+        }
+        if (yParityIn != yParityOut) {
+            for (int i = 0; i < outReadouts->n; i++) {
+                psImage *image = outReadouts->data[i];
+                outReadouts->data[i] = xFlipImage(image);
+                psFree(image);
+            }
+            // Correct CELL.Y 0 for the parity shift
+            psMetadataItem *y0item = psMetadataLookup(inCell->concepts, "CELL.Y0");
+            y0item->data.S32 -= ySize->data.S32[cellNum];
+        }
+
+        //////////////////////////////////////////////////////////////////////////////////////////////////////
+        // DO I WANT TO DO THIS????
+        // Wouldn't it be better to splice all the images together first, and then divvy up the components?
+        //////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+        // Manipulate the images, if required
+        for (int i = 0; i < outReadouts->n; i++) {
+            psImage *image = manipulate(outReadouts
+
+                         psImage *image = psMemIncrRefCounter(outReadouts->data[i]);
+                         if (xParityIn != xParityOut) {
+                             psImage *image = xFlipImage(image);
+                         }
+
+
+        // THIS ISN'T SO SIMPLE: we need to SET the output x0,y0 based on where the image actually gets pasted
+        // Get the offsets
+        x0->data.S32[i] = psMetadataLookupS32(NULL, outCell->concepts, "CELL.X0");
+        y0->data.S32[i] = psMetadataLookupS32(NULL, outCell->concepts, "CELL.Y0");
+
+
+
+
+// Splice a list of cells together
+static psArray *spliceCells(psList *outCells, // List of target cells (required for parity info)
+                            psList *inCells, // List of cells to splice together
+                            bool posDep // Position dependent placement of overscans?
+    )
+{
+    assert(outCells);
+    assert(inCells);
+
+    psTrace(__func__, 5, "Splicing together all cells in the bucket.\n");
+
+    if (inCells->n != outCells->n) {
+        psError(PS_ERR_IO, true, "Sizes of input (%d) and output (%d) lists of cells don't match.\n",
+                inCells->n, outCells->n);
+        return NULL;
+    }
+
+    int numCells = inCells->n;          // Number of cells
+    int readdir = 0;                    // Read direction for CCD; currently unknown
+    int numReadouts = 0;                // Number of readouts in a cell
+    psArray *spliced = NULL;            // Array of spliced readouts
+    psElemType type = 0;                // Image type (U16, S32, F32, etc)
+
+    psVector *xSize = NULL;             // Size of spliced image in x
+    psVector *ySize = NULL;             // Size of spliced image in y
+
+    psVector *xFlip = psVectorAlloc(numCells, PS_TYPE_U8); // Do we need to flip in x?
+    psVector *yFlip = psVectorAlloc(numCells, PS_TYPE_U8); // Do we need to flip in y?
+
+
+    // We go through the cells to make sure everything's kosher, and to get the sizes.
+    psListIterator *inCellsIter = psListIteratorAlloc(inCells, PS_LIST_HEAD, false); // Iterator for cells
+    psListIterator *outCellsIter = psListIteratorAlloc(outCells, PS_LIST_HEAD, false); // Iterator for cells
+    pmCell *inCell = NULL;              // Cell from list of input cells
+    int cellNum = 0;                    // Cell number;
+    while (inCell = psListGetAndIncrement(inCellsIter)) {
+        psArray *readouts = inCell->readouts;   // The readouts comprising the cell
+
+        // Get and check the read direction
+        int cellReaddir = psMetadataLookupS32(NULL, inCell->concepts, "CELL.READDIR");
+        psTrace(__func__, 10, "Checking read direction for cell %d: %d\n", cellNum, cellReaddir);
+        if (cellNum == 0) {
+            // First run through; set it.
+            readdir = cellReaddir;
+        } else if (readdir != cellReaddir) {
+            psError(PS_ERR_IO, true, "Trying to splice cells with different read directions (%d vs %d). I "
+                    "don't know how to do that!\n", readdir, cellReaddir);
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        // Get and check the number of readouts
+        if (! spliced) {
+            numReadouts = readouts->n;
+            spliced = psArrayAlloc(numReadouts);
+            xSize = psVectorAlloc(numReadouts, PS_TYPE_S32);
+            ySize = psVectorAlloc(numReadouts, PS_TYPE_S32);
+            for (int i = 0; i < numReadouts; i++) {
+                xSize->data.S32[i] = 0;
+                ySize->data.S32[i] = 0;
+            }
+        } else if (readouts->n != numReadouts) {
+            psError(PS_ERR_IO, true, "Trying to splice cells with different number of reads (%d vs %d)\n",
+                    numReadouts, readouts->n);
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        // Get and check the cell parities
+        pmCell *outCell = psListGetAndIncrement(outCellsIter);  // Corresponding output cell
+        int xParityIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.XPARITY");
+        int yParityIn = psMetadataLookupS32(NULL, inCell->concepts, "CELL.YPARITY");
+        int xParityOut = psMetadataLookupS32(NULL, outCell->concepts, "CELL.XPARITY");
+        int yParityOut = psMetadataLookupS32(NULL, outCell->concepts, "CELL.YPARITY");
+        if (xParityIn == 0 || xParityOut == 0 || yParityIn == 0 || yParityOut == 0) {
+            psError(PS_ERR_IO, false, "Unable to determine parity of cell!\n");
+            // XXX Clean up before returning
+            return NULL;
+        }
+
+        if (yParityIn == yParityOut) {
+            yFlip->data.U8[cellNum] = 0;
+        } else {
+            psTrace(__func__, 7, "Need to flip y.\n");
+            yFlip->data.U8[cellNum] = 1;
+        }
+        if (xParityIn == xParityOut) {
+            xFlip->data.U8[cellNum] = 0;
+        } else {
+            psTrace(__func__, 7, "Need to flip x.\n");
+            xFlip->data.U8[cellNum] = 1;
+        }
+        cellNum++;
+
+        // Calculate the sizes of the spliced images
+        for (int i = 0; i < numReadouts; i++) {
+            pmReadout *readout = readouts->data[i]; // The readout of interest
+            psImage *image = readout->image; // The image pixels
+
+            // Check image type
+            if (type == 0) {
+                type = image->type.type;
+            } else if (image->type.type != type) {
+                psError(PS_ERR_IO, true, "Trying to splice readouts with different image types (%d vs %d)\n",
+                        type, image->type.type);
+                // XXX Clean up before returning
+                return NULL;
+            }
+
+            psRegion *trimsec = psMetadataLookupPtr(NULL, inCell->concepts, "CELL.TRIMSEC"); // Trim section
+            psList *biassecs = psMetadataLookupPtr(NULL, inCell->concepts, "CELL.BIASSEC"); // Bias section
+            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+            psRegion *biassec = NULL;   // Bias section from list iteration
+
+            // Get the size of the spliced image
+            if (readdir == 1) {
+                psTrace(__func__, 7, "TRIMSEC is %.0fx%.0f\n", trimsec->x1 - trimsec->x0,
+                        trimsec->y1 - trimsec->y0);
+                xSize->data.S32[i] += trimsec->x1 - trimsec->x0;
+                ySize->data.S32[i] = MAX(ySize->data.S32[i], trimsec->y1 - trimsec->y0);
+                while (biassec = psListGetAndIncrement(biassecsIter)) {
+                    psTrace(__func__, 9, "BIASSEC is %.0fx%.0f\n", biassec->x1 - biassec->x0,
+                        biassec->y1 - biassec->y0);
+                    xSize->data.S32[i] += biassec->x1 - biassec->x0;
+                    ySize->data.S32[i] = MAX(ySize->data.S32[i], biassec->y1 - biassec->y0);
+                }
+            } else if (readdir == 2) {
+                ySize->data.S32[i] += trimsec->y1 - trimsec->y0;
+                xSize->data.S32[i] = MAX(xSize->data.S32[i], trimsec->x1 - trimsec->x0);
+                while (biassec = psListGetAndIncrement(biassecsIter)) {
+                    ySize->data.S32[i] += biassec->y1 - biassec->y0;
+                    xSize->data.S32[i] = MAX(ySize->data.S32[i], biassec->x1 - biassec->x0);
+                }
+            } else {
+                psError(PS_ERR_IO, true, "Invalid read direction: %d\n", readdir);
+                // XXX Clean up before returning
+                return NULL;
+            }
+            psFree(biassecsIter);
+        }
+    }
+    psFree(inCellsIter);
+    psFree(outCellsIter);
+
+
+    // Make sure all the readouts have the same size
+    int numRows = ySize->data.S32[0];   // Number of rows for spliced image
+    int numCols = xSize->data.S32[0];   // Number of columns for spliced image
+    for (int i = 1; i < numReadouts; i++) {
+        if (xSize->data.S32[i] != numCols || ySize->data.S32[i] != numRows) {
+            psError(PS_ERR_IO, true, "Spliced readouts would have different sizes: %dx%d vs %dx%d\n",
+                    numCols, numRows, xSize->data.S32[i], ySize->data.S32[i]);
+        }
+    }
+
+    psTrace(__func__, 5, "Spliced image has dimensions %dx%d\n", numCols, numRows);
+
+    // Now we have done the requisite checks, and know the sizes; we just go through and splice together
+    for (int i = 0; i < numReadouts; i++) {
+        psImage *splice = psImageAlloc(numCols, numRows, type); // The spliced image
+        int x0 = 0, y0 = 0;             // Position at which the splice begins
+
+        // Position-dependent overscans first
+        if (posDep) {
+            psTrace(__func__, 8, "Doing first position-dependent biases...\n");
+            for (int cellNum = 0; cellNum < inCells->n / 2; cellNum++) {
+                psTrace(__func__, 9, "Cell %d...\n", cellNum);
+                pmCell *inCell = psListGet(inCells, cellNum); // Input cell of interest
+                pmCell *outCell = psListGet(outCells, cellNum); // Output cell of interest
+                spliceBias(splice, &x0, &y0, inCell, outCell, xFlip->data.U8[i], yFlip->data.U8[i], i,
+                           readdir);
+            }
+        }
+
+        // Then the images
+        psListIterator *inCellsIter = psListIteratorAlloc(inCells, PS_LIST_HEAD, false);// Iterator for cells
+        pmCell *inCell = NULL;          // Cell from the list of cells
+        psListIterator *outCellsIter = psListIteratorAlloc(outCells, PS_LIST_HEAD, false); // Iterator
+        int cellNum = 0;                // Cell number
+        while (inCell = psListGetAndIncrement(inCellsIter)) {
+            psArray *readouts = inCell->readouts; // The readouts comprising the cell
+            pmReadout *readout = readouts->data[i]; // The specific readout of interest
+            pmCell *outCell = psListGetAndIncrement(outCellsIter); // Output cell
+            psRegion *inTrimsec = psMetadataLookupPtr(NULL, inCell->concepts, "CELL.TRIMSEC"); // TRIMSEC
+            psRegion *outTrimsec = spliceImage(splice, &x0, &y0, readout->image, inTrimsec, readdir,
+                                               xFlip->data.U8[cellNum], yFlip->data.U8[cellNum]);
+
+            // Update the output TRIMSEC
+            psMetadataAdd(outCell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_DATA_UNKNOWN | PS_META_REPLACE,
+                             "TRIMSEC from pmFPAMorph", outTrimsec);
+        }
+        psFree(inCellsIter);
+        psFree(outCellsIter);
+
+        // Then the biases
+        if (posDep) {
+            // Need to only do half the biases
+            psTrace(__func__, 8, "Doing second position-dependent biases...\n");
+            for (int cellNum = inCells->n / 2; cellNum < inCells->n; cellNum++) {
+                psTrace(__func__, 9, "Cell %d...\n", cellNum);
+                pmCell *inCell = psListGet(inCells, cellNum); // Input cell of interest
+                pmCell *outCell = psListGet(outCells, cellNum); // Output cell of interest
+                spliceBias(splice, &x0, &y0, inCell, outCell, xFlip->data.U8[i], yFlip->data.U8[i], i,
+                           readdir);
+            }
+        } else {
+            // Need to do all the biases
+            for (int cellNum = 0; cellNum < inCells->n; cellNum++) {
+                pmCell *inCell = psListGet(inCells, cellNum); // Input cell of interest
+                pmCell *outCell = psListGet(outCells, cellNum); // Output cell of interest
+                spliceBias(splice, &x0, &y0, inCell, outCell, xFlip->data.U8[i], yFlip->data.U8[i], i,
+                           readdir);
+            }
+        }
+
+        spliced->data[i] = splice;
+
+    } // Iterating over readouts
+
+    return spliced;
+}
+
+
+/////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+bool pmFPAMorph(pmFPA *toFPA,           // FPA structure to which to morph
+                pmFPA *fromFPA,         // FPA structure from which to morph
+                bool positionDependent, // Is the position of the overscan dependent on the position?
+                int chipNum,            // Chip number, in case the scopes are different
+                int cellNum             // Cell number, in case the scopes are different
+    )
+{
+    psList *targetCells = psListAlloc(NULL); // List of target cells
+    psList *sourceCells = psListAlloc(NULL); // List of source cells
+
+    psArray *toChips = toFPA->chips;    // Array of chips
+    psArray *fromChips = fromFPA->chips;// Array of chips
+
+    psTrace(__func__, 1, "Copying FPA concepts over...\n");
+    pap_psMetadataCopy(toFPA->concepts, fromFPA->concepts);
+    pap_psMetadataCopy(toFPA->phu, fromFPA->phu);
+
+    for (int i = 0; i < toChips->n; i++) {
+        pmChip *toChip = toChips->data[i];
+        pmChip *fromChip = NULL;
+        // Select the correct chip
+        if (toChips->n == 1 && fromChips->n > chipNum) {
+            fromChip = fromChips->data[chipNum];
+        } else if (fromChips->n == 1 && toChips->n > chipNum) {
+            fromChip = fromChips->data[0];
+        } else if (toChips->n == fromChips->n) {
+            fromChip = fromChips->data[i];
+        } else {
+            psError(PS_ERR_IO, true, "Unable to discern intended chip.\n");
+            return false;
+        }
+
+        if (! fromChip->valid) {
+            toChip->valid = false;
+            continue;
+        }
+
+        psTrace(__func__, 2, "Copying chip %d concepts over...\n", i);
+        pap_psMetadataCopy(toChip->concepts, fromChip->concepts);
+
+        psArray *toCells = toChip->cells; // Array of cells
+        psArray *fromCells = fromChip->cells; // Array of cells
+
+        for (int j = 0; j < toCells->n; j++) {
+            pmCell *toCell = toCells->data[j];
+            pmCell *fromCell = NULL;
+            // Select the correct cell
+            if (toCells->n == 1 && fromCells->n > cellNum) {
+                fromCell = fromCells->data[cellNum];
+            } else if (fromCells->n == 1 && toCells->n > cellNum) {
+                fromCell = fromCells->data[0];
+            } else if (toCells->n == fromCells->n) {
+                fromCell = fromCells->data[j];
+            } else {
+                psError(PS_ERR_IO, true, "Unable to discern intended cell.\n");
+                return false;
+            }
+
+            if (! fromCell->valid) {
+                toCell->valid = false;
+                continue;
+            }
+
+#ifdef TESTING
+            int xParityIn = psMetadataLookupS32(NULL, fromCell->concepts, "CELL.XPARITY");
+            int yParityIn = psMetadataLookupS32(NULL, fromCell->concepts, "CELL.YPARITY");
+            int xParityOut = psMetadataLookupS32(NULL, toCell->concepts, "CELL.XPARITY");
+            int yParityOut = psMetadataLookupS32(NULL, toCell->concepts, "CELL.YPARITY");
+            psTrace(__func__, 3, "Chip %d, cell %d: in (%d,%d) out (%d,%d)\n", i, j, xParityIn, yParityIn,
+                    xParityOut, yParityOut);
+#endif
+
+#ifdef TESTING
+            psMetadataPrint(toCell->concepts, 7);
+#endif
+
+            // Copy the concepts over
+
+            // Need to be a little tricky here --- some concepts are already set from the camera configuration
+            // file (e.g., CELL.NAME), and we want to preserve these, not replace it with the old value.
+            psTrace(__func__, 3, "Copying cell %d concepts over...\n", j);
+            psMetadata *newConcepts = pap_psMetadataCopy(NULL, fromCell->concepts);
+            pap_psMetadataCopy(newConcepts, toCell->concepts); // This preserves any already existing concepts
+            psFree(toCell->concepts);
+            toCell->concepts = newConcepts;
+
+            // Now, we need to check if the following concepts for the target cell are defined statically:
+            // CELL.XPARITY, CELL.YPARITY, CELL.XBIN, CELL.YBIN, CELL.X0, CELL.Y0
+            // 1. If they are specified by the header, we can do what we want (because we can set the header).
+            // 2. If they are not specified by the header, we use those values.
+            const char *checkConcepts[4] = { "CELL.XPARITY",
+                                             "CELL.YPARITY",
+                                             "CELL.XBIN",
+                                             "CELL.YBIN",
+                                             "CELL.X0",
+                                             "CELL.Y0" }; // Concepts that we need to check
+            bool mdok = false;          // Result of MD lookup
+            psMetadata *translation = psMetadataLookupMD(&mdok, toFPA->camera, "TRANSLATION"); // Header
+                                        // translation table
+            if (!mdok || ! translation) {
+                psError(PS_ERR_IO, true, "Unable to find TRANSLATION in camera configuration!\n");
+                return false;
+            }
+            for (int c = 0; c < 4; c++) {
+                psString headerName = psMetadataLookupString(&mdok, translation, checkConcepts[c]); // Name of
+                                        // header holding the concept, or NULL
+                if (!mdok || strlen(headerName) <= 0) {
+                    p_pmCellIngestConcept(toFPA, toChip, toCell, db, checkConcepts[c]);
+                }
+            }
+
+#ifdef TESTING
+            psMetadataPrint(toCell->concepts, 7);
+#endif
+
+            psTrace(__func__, 5, "Putting chip %d, cell %d in the bucket.\n", i, j);
+            psListAdd(targetCells, PS_LIST_TAIL, toCell);
+            psListAdd(sourceCells, PS_LIST_TAIL, fromCell);
+
+            // Copy the cell contents over
+            toCell->readouts = psMemIncrRefCounter(fromCell->readouts);
+
+            if (toCell->hdu && strlen(toCell->hdu->extname) > 0) {
+                // Splice the component cells
+                p_pmHDU *hdu = toCell->hdu;
+                if (! hdu->header) {
+                    hdu->header = psMetadataAlloc();
+                }
+                hdu->images = spliceCells(targetCells, sourceCells, positionDependent);
+                // Purge the lists
+                while (psListRemove(targetCells, PS_LIST_TAIL));
+                while (psListRemove(sourceCells, PS_LIST_TAIL));
+            }
+
+        }
+
+        if (toChip->hdu && strlen(toChip->hdu->extname) > 0) {
+            // Splice the component cells
+            p_pmHDU *hdu = toChip->hdu;
+            if (! hdu->header) {
+                hdu->header = psMetadataAlloc();
+            }
+            hdu->images = spliceCells(targetCells, sourceCells, positionDependent);
+            // Purge the lists
+            while (psListRemove(targetCells, PS_LIST_TAIL));
+            while (psListRemove(sourceCells, PS_LIST_TAIL));
+        }
+
+    }
+
+    if (toFPA->hdu && strlen(toFPA->hdu->extname) > 0) {
+        // Splice the component cells
+        p_pmHDU *hdu = toFPA->hdu;
+        if (! hdu->header) {
+            hdu->header = psMetadataAlloc();
+        }
+        hdu->images = spliceCells(targetCells, sourceCells, positionDependent);
+        // Purge the lists
+        while (psListRemove(targetCells, PS_LIST_TAIL));
+        while (psListRemove(sourceCells, PS_LIST_TAIL));
+    }
+
+    return true;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAMorph.h	(revision 7462)
@@ -0,0 +1,13 @@
+#ifndef PM_FPA_MORPH_H
+#define PM_FPA_MORPH_H
+
+// Morph one focal plane representation into another
+bool pmFPAMorph(pmFPA *toFPA,		// FPA structure to which to morph
+		pmFPA *fromFPA,		// FPA structure from which to morph
+		bool positionDependent, // Is the position of the overscan dependent on the position?
+		int chipNum,		// Chip number, in case the scopes are different
+		int cellNum		// Cell number, in case the scopes are different
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.c	(revision 7462)
@@ -0,0 +1,636 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPAConceptsGet.h"
+#include "pmFPARead.h"
+
+#include "papStuff.h"                   // For "split"
+
+// NOTE: Need to deal with header inheritance
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// Read a FITS extension into a chip
+static bool readExtension(p_pmHDU *hdu, // Pixel data into which to read
+                          psFits *fits  // The FITS file from which to read
+    )
+{
+    const char *extName = hdu->extname; // Extension name
+
+    psTrace(__func__, 7, "Moving to extension %s...\n", extName);
+    if (strncmp(extName, "PHU", 3) == 0) {
+        if (!psFitsMoveExtNum(fits, 0, false)) {
+            psError(PS_ERR_IO, false, "Unable to find PHU in FITS file!\n");
+            return false;
+        }
+    } else if (! psFitsMoveExtName(fits, extName)) {
+        psError(PS_ERR_IO, false, "Unable to find extension %s in FITS file!\n", extName);
+        return false;
+    }
+    psTrace(__func__, 7, "Reading header....\n");
+    psMetadata *header = psFitsReadHeader(NULL, fits); // Header
+    if (! header) {
+        psError(PS_ERR_IO, false, "Unable to read FITS header!\n");
+        return false;
+    }
+
+    psTrace(__func__, 7, "Checking NAXIS....\n");
+    bool mdStatus = false;
+    int nAxis = psMetadataLookupS32(&mdStatus, header, "NAXIS");
+    if (!mdStatus) {
+        psLogMsg(__func__, PS_LOG_WARN, "There is no NAXIS keyword in the FITS header of extension %s!\n",
+                 extName);
+    }
+    if (nAxis != 2 && nAxis != 3) {
+        psLogMsg(__func__, PS_LOG_WARN, "Image is not 2- or 3-dimensional --- reading into a single image "
+                 "anyway.\n");
+    }
+    psTrace(__func__, 9, "NAXIS = %d\n", nAxis);
+
+    int numPlanes = 1;                  // Number of planes
+    if (nAxis == 3) {
+        numPlanes = psMetadataLookupS32(&mdStatus, header, "NAXIS3");
+        if (!mdStatus) {
+            psError(PS_ERR_IO, false, "Unable to read NAXIS3 for 3-dimensional image!\n");
+            return false;
+        }
+    }
+    psRegion region = {0, 0, 0, 0};     // Region to read is everything
+
+    // Read each plane into the array
+    psTrace(__func__, 7, "Reading %d planes into array....\n", numPlanes);
+    psArray *pixels = psArrayAlloc(numPlanes); // Array of images
+    for (int i = 0; i < numPlanes; i++) {
+        psTrace(__func__, 9, "Reading plane %d\n", i);
+        psMemCheckCorruption(true);
+        psImage *image = psFitsReadImage(NULL, fits, region, i);
+
+        // XXX: Type conversion here to support the modules, which don't have multiple type support yet
+        if (image->type.type != PS_TYPE_F32) {
+            pixels->data[i] = psImageCopy(NULL, image, PS_TYPE_F32);
+
+#ifndef PRODUCTION
+            // XXX: Temporary fix for writing images, until psFits gets cleaned up
+            psMetadataItem *bitpixItem = psMetadataLookup(header, "BITPIX");
+            bitpixItem->data.S32 = -32;
+            psMetadataRemove(header, 0, "BZERO");
+            psMetadataRemove(header, 0, "BSCALE");
+#endif
+
+            psFree(image);
+        } else {
+            pixels->data[i] = image;
+        }
+        psTrace(__func__, 10, "Done\n");
+        if (! pixels->data[i]) {
+            psError(PS_ERR_IO, false, "Unable to read image for extension %s, plane %d\n", extName, i);
+            return false;
+        }
+    }
+
+    // Update the HDU with the new data
+    hdu->images = pixels;
+    hdu->header = header;
+
+    // XXX: Insert mask and weight reading here
+
+    return true;
+}
+
+// Portion out an image into the cell
+static bool generateReadouts(pmCell *cell, // The cell that gets its bits
+                             p_pmHDU *hdu // Pixel data, containing image, mask, weights
+    )
+{
+    psArray *images = hdu->images;      // Array of images (each of which is a readout)
+    psArray *masks = hdu->masks;        // Array of masks (one for each readout)
+    // Iterate over each of the image planes
+    for (int i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // The i-th plane
+        psImage *mask = NULL;           // The mask
+        if (masks) {
+            mask = masks->data[i];
+        }
+
+        pmReadout *readout = pmReadoutAlloc(cell, image, mask, 0,0,1,1);
+        psFree(readout);                // This seems silly, but the alloc registers it with the cell, so we
+                                        // don't have to do anything with it.  Perhaps we should change
+                                        // pmReadoutAlloc to pmReadoutAdd?
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmFPARead(pmFPA *fpa,              // FPA to read into
+               psFits *fits,            // FITS file from which to read
+               psMetadata *phu,         // Primary header
+               psDB *db                 // Database handle, for concept ingest
+    )
+{
+    p_pmHDU *hdu = NULL;        // Pixel data from FITS file
+
+    // Read the PHU, if required
+    if (! phu) {
+        if (! psFitsMoveExtNum(fits, 0, false)) {
+            psError(PS_ERR_IO, false, "Unable to find PHU in FITS file!\n");
+            return false;
+        }
+        psTrace(__func__, 7, "Reading PHU....\n");
+        fpa->phu = psFitsReadHeader(NULL, fits); // Primary header
+    } else {
+        fpa->phu = psMemIncrRefCounter(phu);
+    }
+
+    // Read in....
+    psTrace(__func__, 1, "Working on FPA...\n");
+    if (fpa->hdu) {
+        hdu = fpa->hdu;
+        psTrace(__func__, 3, "Reading pixels from extension %s into FPA.\n", hdu->extname);
+        if (! readExtension(hdu, fits)) {
+            psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+    pmFPAIngestConcepts(fpa, db);
+
+    psArray *chips = fpa->chips;        // Array of chips
+    // Iterate over the FPA
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i]; // The chip
+
+        // Only read chips marked "valid"
+        if (! chip->valid) {
+            psTrace(__func__, 2, "Ignoring chip %d...\n", i);
+            continue;
+        }
+        psTrace(__func__, 2, "Reading in chip %d...\n", i);
+
+        if (chip->hdu) {
+            hdu = chip->hdu;
+            psTrace(__func__, 3, "Reading pixels from extension %s into chip %d.\n", hdu->extname, i);
+            if (! readExtension(hdu, fits)) {
+                psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n", hdu->extname);
+                return false;
+            }
+        }
+        pmChipIngestConcepts(chip, db);
+
+        // Iterate over the chip
+        psArray *cells = chip->cells;   // Array of cells
+        for (int j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[j]; // The cell
+
+            // Only read cells marked "valid"
+            if (! cell->valid) {
+                psTrace(__func__, 3, "Ignoring chip %d...\n", i);
+                continue;
+            }
+            psTrace(__func__, 3, "Reading in cell %d...\n", j);
+
+            if (cell->hdu) {
+                hdu = cell->hdu;
+                psTrace(__func__, 5, "Reading pixels from extension %s into cell %d.\n", hdu->extname,
+                        j);
+                if (! readExtension(hdu, fits)) {
+                    psError(PS_ERR_IO, false, "Unable to read pixels from extension %s\n",
+                            hdu->extname);
+                    return false;
+                }
+            }
+            pmCellIngestConcepts(cell, db);
+
+            psTrace(__func__, 5, "Allocating readouts for chip %d cell %d...\n",
+                    i, j);
+            generateReadouts(cell, hdu);
+        }
+    }
+
+    return true;
+}
+
+// Translate a name from the configuration file, containing something like "%a_%d" to a real value
+psString p_pmFPATranslateName(psString name, // The name to translate
+                              pmCell *cell // The cell for which to translate
+    )
+{
+    // %a is the FPA.NAME
+    // %b is the CHIP.NAME
+    // %c is the CELL.NAME
+    // %d is the chip number
+    // %e if the cell number
+    // %f is the extension name
+
+    psString translation = psMemIncrRefCounter(name); // The translated string
+    char *temp = NULL;                  // Temporary string
+
+    pmChip *chip = cell->parent;        // Chip of interest
+    pmFPA *fpa = chip->parent;          // FPA of interest
+
+    // FPA.NAME
+    if (temp = strstr(translation, "%a")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        psString fpaName = psMetadataLookupString(NULL, fpa->concepts, "FPA.NAME"); // The value of FPA.NAME
+        psStringAppend(&translation, "%s%s", fpaName, temp + 2);
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    // CHIP.NAME
+    if (temp = strstr(translation, "%b")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        psString chipName = psMetadataLookupString(NULL, chip->concepts, "CHIP.NAME"); // CHIP.NAME's value
+        psStringAppend(&translation, "%s%s", chipName, temp + 2);
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    // CELL.NAME
+    if (temp = strstr(translation, "%c")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        psString cellName = psMetadataLookupString(NULL, cell->concepts, "CELL.NAME"); // CELL.NAME's value
+        psStringAppend(&translation, "%s%s", cellName, temp + 2);
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    // Chip number
+    if (temp = strstr(translation, "%d")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        // Search for the pointer to get the chip number
+        int chipNum = -1;
+        psArray *chips = fpa->chips;    // The array of chips
+        for (int i = 0; i < chips->n && chipNum < 0; i++) {
+            if (chips->data[i] == chip) {
+                chipNum = i;
+            }
+        }
+        if (chipNum < 0) {
+            psError(PS_ERR_IO, true, "Unable to find chip to get name: %s\n", name);
+            // Try to muddle on by leaving the number out
+            psStringAppend(&translation, "%s", temp + 2);
+        } else {
+            psStringAppend(&translation, "%d%s", chipNum, temp + 2);
+        }
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    // Cell number
+    if (temp = strstr(translation, "%e")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        // Search for the pointer to get the cell number
+        int cellNum = -1;
+        psArray *cells = chip->cells;   // The array of cells
+        for (int i = 0; i < cells->n && cellNum < 0; i++) {
+            if (cells->data[i] == cell) {
+                cellNum = i;
+            }
+        }
+        if (cellNum < 0) {
+            psError(PS_ERR_IO, true, "Unable to find cell to get name: %s\n", name);
+            // Try to muddle on by leaving the number out
+            psStringAppend(&translation, "%s", temp + 2);
+        } else {
+            psStringAppend(&translation, "%d%s", cellNum, temp + 2);
+        }
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    // Extension name
+    if (temp = strstr(translation, "%f")) {
+        // This is not particularly friendly to the memory allocation, but the cache should make it OK,
+        // and there's not much of it anyway...
+        psFree(translation);
+        translation = psStringNCopy(name, strlen(name) - strlen(temp)); // Copy first part of string
+        const char *extname = NULL;
+        if (cell->hdu) {
+            extname = cell->hdu->extname;
+        } else if (chip->hdu) {
+            extname = chip->hdu->extname;
+        } else if (fpa->hdu) {
+            extname = fpa->hdu->extname;
+        }
+        psStringAppend(&translation, "%s%s", extname, temp + 2);
+        // So "translation" now contains the first part, the replaced string, and the last part.
+    }
+
+    return translation;
+}
+
+// Read a mask into the FPA
+bool pmFPAReadMask(pmFPA *fpa,          // FPA to read into
+                   psFits *source       // Source FITS file (for the original data)
+    )
+{
+    const psMetadata *camera = fpa->camera; // Camera configuration for FPA
+    bool mdok = false;                  // Status of MD lookup
+
+    // Get the required information from the camera configuration
+    psMetadata *supps = psMetadataLookupMD(&mdok, camera, "SUPPLEMENTARY"); // Rules for supplementary data
+    if (! mdok || ! supps) {
+        psError(PS_ERR_IO, false, "Unable to find SUPPLEMENTARY in camera configuration!\n");
+        return false;
+    }
+    psString sourceType = psMetadataLookupString(&mdok, supps, "MASK.SOURCE"); // Type of source: EXT | FILE
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find MASK.SOURCE in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+    psString name = psMetadataLookupString(&mdok, supps, "MASK.NAME"); // Name of mask
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find MASK.NAME in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+
+    // Go through the FPA to each cell/readout to get the mask
+    p_pmHDU *hdu = fpa->hdu;            // The HDU into which we will read the mask
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int chipNum = 0; chipNum < chips->n; chipNum++) {
+        pmChip *chip = chips->data[chipNum]; // The current chip of interest
+        if (chip->valid) {
+            if (chip->hdu) {
+                hdu = chip->hdu;
+            }
+            psArray *cells = chip->cells;       // Array of cells
+            for (int cellNum = 0; cellNum < cells->n; cellNum++) {
+                pmCell *cell = cells->data[cellNum]; // The current cell of interest
+                if (cell->valid) {
+                    if (cell->hdu) {
+                        hdu = cell->hdu;
+                    }
+
+                    // Now, need to find out where to get the pixels
+                    psFits *maskSource = psMemIncrRefCounter(source); // Source of mask image
+                    if (strcasecmp(sourceType, "FILE") == 0) {
+                        // Source is a file (with optional extension, e.g., "myMaskFile.fits:thisExt"
+                        psString filenameExt = p_pmFPATranslateName(name, cell);
+                        char *colon = strchr(filenameExt, ':'); // Pointer to a colon in the filename-extn
+                        psString filename = NULL; // The filename
+                        psString extname = NULL;// The extenstion name
+                        if (colon) {
+                            filename = psStringNCopy(filenameExt, strlen(filenameExt) - strlen(colon));
+                            if (strlen(colon) > 1) {
+                                extname = psStringCopy(colon + 1);
+                            }
+                        } else {
+                            filename = psMemIncrRefCounter(filenameExt);
+                        }
+
+                        psFree(maskSource);
+                        maskSource = psFitsOpen(filename, "r");
+                        if (extname) {
+                            if (! psFitsMoveExtName(maskSource, extname)) {
+                                psLogMsg(__func__, PS_LOG_WARN, "Unable to find extension %s to read mask.\n",
+                                         extname);
+                                return false;
+                            }
+                        }
+                        psFree(filename);
+                        psFree(extname);
+                        psFree(filenameExt);
+                    } else if (strncasecmp(sourceType, "EXT", 3) == 0) {
+                        // Source is an extension in the original file
+                        psString extname = p_pmFPATranslateName(name, cell);
+                        psFitsMoveExtName(maskSource, extname);
+                    }
+
+                    // We've arrived where the pixels are.  Now we need to read them in.
+                    psMetadata *header = psFitsReadHeader(NULL, maskSource); // The header
+                    bool mdStatus = false;
+                    int nAxis = psMetadataLookupS32(&mdStatus, header, "NAXIS");
+                    if (!mdStatus) {
+                        psLogMsg(__func__, PS_LOG_WARN, "There is no NAXIS keyword in the FITS header for "
+                                 "mask (%s)!\n", name);
+                    }
+                    if (nAxis != 2 && nAxis != 3) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Image is not 2- or 3-dimensional --- reading into "
+                                 "a single image anyway.\n");
+                    }
+
+                    int numPlanes = 1;  // Number of planes
+                    if (nAxis == 3) {
+                        numPlanes = psMetadataLookupS32(&mdStatus, header, "NAXIS3");
+                        if (!mdStatus) {
+                            psError(PS_ERR_IO, false, "Unable to read NAXIS3 for 3-dimensional image!\n");
+                            // Try to proceed by taking only the first plane
+                            numPlanes = 1;
+                        }
+                        if (numPlanes != 1 && numPlanes != cell->readouts->n) {
+                            psError(PS_ERR_IO, false, "Number of masks (%d) does not match number of "
+                                    "readouts (%d)\n", numPlanes, cell->readouts->n);
+                            // Try to proceed by taking only the first plane
+                            numPlanes = 1;
+                        }
+                    }
+
+                    hdu->masks = psArrayAlloc(hdu->images->n);
+
+                    // Read each plane into the array
+                    psArray *readouts = cell->readouts; // The array of readouts
+                    for (int i = 0; i < hdu->masks->n; i++) {
+                        psImage *mask = NULL; // The mask to be added
+                        if (i < numPlanes) {
+                            // Read the mask from the file
+                            psTrace(__func__, 9, "Reading plane %d\n", i);
+                            psRegion region = {0, 0, 0, 0};
+                            mask = psFitsReadImage(NULL, maskSource, region, i);
+                        } else {
+                            // One mask in the file is provided for all planes in the original image
+                            psTrace(__func__, 9, "Copying plane 0 into plane %d\n", i);
+                            psImage *original = hdu->masks->data[0];
+                            mask = psImageCopy(NULL, original, original->type.type);
+                        }
+                        hdu->masks->data[0] = mask;
+                        pmReadout *readout = readouts->data[i];
+                        readout->mask = mask;
+                        // Check the dimensions
+                        // XXX: Perhaps this check should be against the CELL.TRIMSEC, not the image size?
+                        if (mask->numCols < readout->image->numCols ||
+                            mask->numRows < readout->image->numRows)
+                        {
+                            psError(PS_ERR_IO, false, "Mask size (%dx%d) not compatible with image (%dx%d)\n",
+                                    mask->numCols, mask->numRows, readout->image->numCols,
+                                    readout->image->numRows);
+                            return false;
+                        }
+                    } // Iterating over readouts
+                    psFree(maskSource);
+                } // Valid cells
+            } // Iterating over cells
+        } // Valid chips
+    } // Iterating over chips
+
+    return true;
+}
+
+
+// Read a mask into the FPA
+// This is just a copy of the above pmFPAReadMask, replacing "mask" with "weight" throughout.
+bool pmFPAReadWeight(pmFPA *fpa,        // FPA to read into
+                     psFits *source     // Source FITS file (for the original data)
+    )
+{
+    const psMetadata *camera = fpa->camera; // Camera configuration for FPA
+    bool mdok = false;                  // Status of MD lookup
+
+    // Get the required information from the camera configuration
+    psMetadata *supps = psMetadataLookupMD(&mdok, camera, "SUPPLEMENTARY"); // Rules for supplementary data
+    if (! mdok || ! supps) {
+        psError(PS_ERR_IO, false, "Unable to find SUPPLEMENTARY in camera configuration!\n");
+        return false;
+    }
+    psString sourceType = psMetadataLookupString(&mdok, supps, "WEIGHT.SOURCE"); // Type of source: EXT | FILE
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find WEIGHT.SOURCE in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+    psString name = psMetadataLookupString(&mdok, supps, "WEIGHT.NAME"); // Name of weight
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find WEIGHT.NAME in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+
+    // Go through the FPA to each cell/readout to get the weight
+    p_pmHDU *hdu = fpa->hdu;            // The HDU into which we will read the weight
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int chipNum = 0; chipNum < chips->n; chipNum++) {
+        pmChip *chip = chips->data[chipNum]; // The current chip of interest
+        if (chip->valid) {
+            if (chip->hdu) {
+                hdu = chip->hdu;
+            }
+            psArray *cells = chip->cells;       // Array of cells
+            for (int cellNum = 0; cellNum < cells->n; cellNum++) {
+                pmCell *cell = cells->data[cellNum]; // The current cell of interest
+                if (cell->valid) {
+                    if (cell->hdu) {
+                        hdu = cell->hdu;
+                    }
+
+                    // Now, need to find out where to get the pixels
+                    psFits *weightSource = psMemIncrRefCounter(source); // Source of weight image
+                    if (strcasecmp(sourceType, "FILE") == 0) {
+                        // Source is a file (with optional extension, e.g., "myWeightFile.fits:thisExt"
+                        psString filenameExt = p_pmFPATranslateName(name, cell);
+                        char *colon = strchr(filenameExt, ':'); // Pointer to a colon in the filename-extn
+                        psString filename = NULL; // The filename
+                        psString extname = NULL;// The extenstion name
+                        if (colon) {
+                            filename = psStringNCopy(filenameExt, strlen(filenameExt) - strlen(colon));
+                            if (strlen(colon) > 1) {
+                                extname = psStringCopy(colon + 1);
+                            }
+                        } else {
+                            filename = psMemIncrRefCounter(filenameExt);
+                        }
+
+                        psFree(weightSource);
+                        weightSource = psFitsOpen(filename, "r");
+                        if (extname) {
+                            if (! psFitsMoveExtName(weightSource, extname)) {
+                                psLogMsg(__func__, PS_LOG_WARN, "Unable to find extension %s to read "
+                                         "weight.\n", extname);
+                                return false;
+                            }
+                        }
+                        psFree(filename);
+                        psFree(extname);
+                        psFree(filenameExt);
+                    } else if (strncasecmp(sourceType, "EXT", 3) == 0) {
+                        // Source is an extension in the original file
+                        psString extname = p_pmFPATranslateName(name, cell);
+                        psFitsMoveExtName(weightSource, extname);
+                    }
+
+                    // We've arrived where the pixels are.  Now we need to read them in.
+                    psMetadata *header = psFitsReadHeader(NULL, weightSource); // The header
+                    bool mdStatus = false;
+                    int nAxis = psMetadataLookupS32(&mdStatus, header, "NAXIS");
+                    if (!mdStatus) {
+                        psLogMsg(__func__, PS_LOG_WARN, "There is no NAXIS keyword in the FITS header for "
+                                 "weight (%s)!\n", name);
+                    }
+                    if (nAxis != 2 && nAxis != 3) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Image is not 2- or 3-dimensional --- reading into "
+                                 "a single image anyway.\n");
+                    }
+
+                    int numPlanes = 1;  // Number of planes
+                    if (nAxis == 3) {
+                        numPlanes = psMetadataLookupS32(&mdStatus, header, "NAXIS3");
+                        if (!mdStatus) {
+                            psError(PS_ERR_IO, false, "Unable to read NAXIS3 for 3-dimensional image!\n");
+                            // Try to proceed by taking only the first plane
+                            numPlanes = 1;
+                        }
+                        if (numPlanes != 1 && numPlanes != cell->readouts->n) {
+                            psError(PS_ERR_IO, false, "Number of weights (%d) does not match number of "
+                                    "readouts (%d)\n", numPlanes, cell->readouts->n);
+                            // Try to proceed by taking only the first plane
+                            numPlanes = 1;
+                        }
+                    }
+
+                    hdu->weights = psArrayAlloc(hdu->images->n);
+
+                    // Read each plane into the array
+                    psArray *readouts = cell->readouts; // The array of readouts
+                    for (int i = 0; i < hdu->weights->n; i++) {
+                        psImage *weight = NULL; // The weight to be added
+                        if (i < numPlanes) {
+                            // Read the weight from the file
+                            psTrace(__func__, 9, "Reading plane %d\n", i);
+                            psRegion region = {0, 0, 0, 0};
+                            weight = psFitsReadImage(NULL, weightSource, region, i);
+                        } else {
+                            // One weight in the file is provided for all planes in the original image
+                            psTrace(__func__, 9, "Copying plane 0 into plane %d\n", i);
+                            psImage *original = hdu->weights->data[0];
+                            weight = psImageCopy(NULL, original, original->type.type);
+                        }
+                        hdu->weights->data[0] = weight;
+                        pmReadout *readout = readouts->data[i];
+                        readout->weight = weight;
+                        // Check the dimensions
+                        // XXX: Perhaps this check should be against the CELL.TRIMSEC, not the image size?
+                        if (weight->numCols < readout->image->numCols ||
+                            weight->numRows < readout->image->numRows)
+                        {
+                            psError(PS_ERR_IO, false, "Weight size (%dx%d) not compatible with image "
+                                    "(%dx%d)\n", weight->numCols, weight->numRows, readout->image->numCols,
+                                    readout->image->numRows);
+                            return false;
+                        }
+                    } // Iterating over readouts
+                    psFree(weightSource);
+                } // Valid cells
+            } // Iterating over cells
+        } // Valid chips
+    } // Iterating over chips
+
+    return true;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPARead.h	(revision 7462)
@@ -0,0 +1,25 @@
+#ifndef PM_FPA_READ_H
+#define PM_FPA_READ_H
+
+#include "pmFPA.h"
+
+bool pmFPARead(pmFPA *fpa,		// FPA to read into
+	       psFits *fits,		// FITS file from which to read
+	       psMetadata *phu,		// Primary header
+	       psDB *db			// Database handle, for concept ingest
+    );
+
+psString p_pmFPATranslateName(psString name, // The name to translate
+			      pmCell *cell // The cell for which to translate
+    );
+
+bool pmFPAReadMask(pmFPA *fpa,		// FPA to read into
+		   psFits *source	// Source FITS file (for the original data)
+    );
+
+bool pmFPAReadWeight(pmFPA *fpa,	// FPA to read into
+		     psFits *source	// Source FITS file (for the original data)
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.c	(revision 7462)
@@ -0,0 +1,280 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPAConceptsSet.h"
+#include "pmFPARead.h"
+
+static bool writeHDU(psFits *fits,      // FITS file to which to write
+                     p_pmHDU *hdu       // Pixel data to write
+    )
+{
+    bool status = true;                 // Status of write, to return
+    for (int i = 0; i < hdu->images->n; i++) {
+        status &= psFitsWriteImage(fits, hdu->header, hdu->images->data[i], i);
+        // XXX: Insert here the writing on mask and weight images
+    }
+
+    return status;
+}
+
+
+bool pmFPAWrite(psFits *fits,           // FITS file to which to write
+                pmFPA *fpa,             // FPA to write
+                psDB *db                // Database to update
+    )
+{
+    bool status = true;                 // Status of writing, to return
+
+    psTrace(__func__, 7, "Outgesting FPA concepts...\n");
+    pmFPAOutgestConcepts(fpa, db);
+
+    // Write the primary header
+    if (fpa->phu) {
+        status &= psFitsMoveExtNum(fits, 0, false);
+        status &= psFitsWriteHeader(fpa->phu, fits);
+    }
+
+    psArray *chips = fpa->chips;        // Array of component chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The component chip
+        if (chip->valid) {
+            psTrace(__func__, 1, "Writing out chip %d...\n", i);
+
+            pmChipOutgestConcepts(chip, db);
+
+            psArray *cells = chip->cells;       // Array of component cells
+            for (int j = 0; j < cells->n; j++) {
+                pmCell *cell = cells->data[j]; // The component cell
+                if (cell->valid) {
+                    psTrace(__func__, 2, "Writing out cell, %d...\n", j);
+
+                    pmCellOutgestConcepts(cell, db);
+
+                    if (cell->hdu && strlen(cell->hdu->extname) > 0) {
+                        status &= writeHDU(fits, cell->hdu);
+                    }
+                }
+            }
+
+            if (chip->hdu && strlen(chip->hdu->extname) > 0) {
+                status &= writeHDU(fits, chip->hdu);
+            }
+        }
+
+    }
+
+    if (fpa->hdu && strlen(fpa->hdu->extname) > 0) {
+        status &= writeHDU(fits, fpa->hdu);
+    }
+
+
+
+    return status;
+}
+
+
+bool pmFPAWriteMask(pmFPA *fpa,         // FPA containing mask to write
+                    psFits *fits        // FITS file for image
+    )
+{
+    const psMetadata *camera = fpa->camera; // Camera configuration for FPA
+    bool mdok = false;                  // Status of MD lookup
+
+    // Get the required information from the camera configuration
+    psMetadata *supps = psMetadataLookupMD(&mdok, camera, "SUPPLEMENTARY"); // Rules for supplementary data
+    if (! mdok || ! supps) {
+        psError(PS_ERR_IO, false, "Unable to find SUPPLEMENTARY in camera configuration!\n");
+        return false;
+    }
+    psString sourceType = psMetadataLookupString(&mdok, supps, "MASK.SOURCE"); // Type of source: EXT | FILE
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find MASK.SOURCE in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+    psString name = psMetadataLookupString(&mdok, supps, "MASK.NAME"); // Name of mask
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find MASK.NAME in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+
+    // Go through the FPA to each cell/readout to get the mask
+    p_pmHDU *hdu = fpa->hdu;            // The HDU into which we will read the mask
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int chipNum = 0; chipNum < chips->n; chipNum++) {
+        pmChip *chip = chips->data[chipNum]; // The current chip of interest
+        if (chip->valid) {
+            if (chip->hdu) {
+                hdu = chip->hdu;
+            }
+            psArray *cells = chip->cells;       // Array of cells
+            for (int cellNum = 0; cellNum < cells->n; cellNum++) {
+                pmCell *cell = cells->data[cellNum]; // The current cell of interest
+                if (cell->valid) {
+                    if (cell->hdu) {
+                        hdu = cell->hdu;
+                    }
+
+                    // Now, need to find out where to write the pixels
+                    psFits *maskDest = psMemIncrRefCounter(fits); // Destination of mask image
+                    psMetadata *header = psMetadataAlloc(); // A dummy header, containing the extension name
+                    if (strcasecmp(sourceType, "FILE") == 0) {
+                        // Source is a file (with optional extension, e.g., "myMaskFile.fits:thisExt"
+                        psString filenameExt = p_pmFPATranslateName(name, cell);
+                        char *colon = strchr(filenameExt, ':'); // Pointer to a colon in the filename-extn
+                        psString filename = NULL; // The filename
+                        psString extname = NULL;// The extenstion name
+                        if (colon) {
+                            filename = psStringNCopy(filenameExt, strlen(filenameExt) - strlen(colon));
+                            if (strlen(colon) > 1) {
+                                extname = psStringCopy(colon + 1);
+                            }
+                        } else {
+                            filename = psMemIncrRefCounter(filenameExt);
+                        }
+
+                        psFree(maskDest);
+                        maskDest = psFitsOpen(filename, "rw");
+                        if (extname) {
+                            psMetadataAddStr(header, PS_LIST_TAIL, "EXTNAME", 0, "Extension name", extname);
+                        }
+                        psFree(filename);
+                        psFree(extname);
+                        psFree(filenameExt);
+                    } else if (strncasecmp(sourceType, "EXT", 3) == 0) {
+                        // Source is an extension in the original file
+                        psString extname = p_pmFPATranslateName(name, cell);
+                        psMetadataAddStr(header, PS_LIST_TAIL, "EXTNAME", 0, "Extension name", extname);
+                        psFree(extname);
+                    }
+
+                    // We've arrived where the pixels are.  Now we need to write them out.
+                    psArray *readouts = cell->readouts; // The array of readouts
+                    for (int readNum = 0; readNum < readouts->n; readNum++) {
+                        pmReadout *readout = readouts->data[readNum]; // The readout of interest
+                        if (! readout->mask) {
+                            psLogMsg(__func__, PS_LOG_WARN, "No mask to write out in %d,%d,%d\n",
+                                     chipNum, cellNum, readNum);
+                        } else {
+                            // XXX: Need to add the extname to the existing header
+                            if (! psFitsWriteImage(maskDest, header, readout->mask, readNum)) {
+                                psError(PS_ERR_IO, false, "Unable to write mask plane %d in extension %s\n",
+                                        readNum, hdu->extname);
+                                return false;
+                            }
+                        }
+                    } // Iterating over readouts
+                    psFree(header);
+                    psFree(maskDest);
+                } // Valid cells
+            } // Iterating over cells
+        } // Valid chips
+    } // Iterating over chips
+
+    return true;
+}
+
+
+bool pmFPAWriteWeight(pmFPA *fpa,       // FPA containing mask to write
+                      psFits *fits      // FITS file for image
+    )
+{
+    const psMetadata *camera = fpa->camera; // Camera configuration for FPA
+    bool mdok = false;                  // Status of MD lookup
+
+    // Get the required information from the camera configuration
+    psMetadata *supps = psMetadataLookupMD(&mdok, camera, "SUPPLEMENTARY"); // Rules for supplementary data
+    if (! mdok || ! supps) {
+        psError(PS_ERR_IO, false, "Unable to find SUPPLEMENTARY in camera configuration!\n");
+        return false;
+    }
+    psString sourceType = psMetadataLookupString(&mdok, supps, "WEIGHT.SOURCE"); // Type of source: EXT | FILE
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find WEIGHT.SOURCE in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+    psString name = psMetadataLookupString(&mdok, supps, "WEIGHT.NAME"); // Name of weight
+    if (! mdok || strlen(sourceType) <= 0) {
+        psError(PS_ERR_IO, false, "Unable to find WEIGHT.NAME in SUPPLEMENTARY section of camera "
+                "configuration!\n");
+        return false;
+    }
+
+    // Go through the FPA to each cell/readout to get the weight
+    p_pmHDU *hdu = fpa->hdu;            // The HDU into which we will read the weight
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int chipNum = 0; chipNum < chips->n; chipNum++) {
+        pmChip *chip = chips->data[chipNum]; // The current chip of interest
+        if (chip->valid) {
+            if (chip->hdu) {
+                hdu = chip->hdu;
+            }
+            psArray *cells = chip->cells;       // Array of cells
+            for (int cellNum = 0; cellNum < cells->n; cellNum++) {
+                pmCell *cell = cells->data[cellNum]; // The current cell of interest
+                if (cell->valid) {
+                    if (cell->hdu) {
+                        hdu = cell->hdu;
+                    }
+
+                    // Now, need to find out where to write the pixels
+                    psFits *weightDest = psMemIncrRefCounter(fits); // Destination of weight image
+                    psMetadata *header = psMetadataAlloc(); // A dummy header, containing the extension name
+                    if (strcasecmp(sourceType, "FILE") == 0) {
+                        // Source is a file (with optional extension, e.g., "myWeightFile.fits:thisExt"
+                        psString filenameExt = p_pmFPATranslateName(name, cell);
+                        char *colon = strchr(filenameExt, ':'); // Pointer to a colon in the filename-extn
+                        psString filename = NULL; // The filename
+                        psString extname = NULL;// The extenstion name
+                        if (colon) {
+                            filename = psStringNCopy(filenameExt, strlen(filenameExt) - strlen(colon));
+                            if (strlen(colon) > 1) {
+                                extname = psStringCopy(colon + 1);
+                            }
+                        } else {
+                            filename = psMemIncrRefCounter(filenameExt);
+                        }
+
+                        psFree(weightDest);
+                        weightDest = psFitsOpen(filename, "rw");
+                        if (extname) {
+                            psMetadataAddStr(header, PS_LIST_TAIL, "EXTNAME", 0, "Extension name", extname);
+                        }
+                        psFree(filename);
+                        psFree(extname);
+                        psFree(filenameExt);
+                    } else if (strncasecmp(sourceType, "EXT", 3) == 0) {
+                        // Source is an extension in the original file
+                        psString extname = p_pmFPATranslateName(name, cell);
+                        psMetadataAddStr(header, PS_LIST_TAIL, "EXTNAME", 0, "Extension name", extname);
+                        psFree(extname);
+                    }
+
+                    // We've arrived where the pixels are.  Now we need to write them out.
+                    psArray *readouts = cell->readouts; // The array of readouts
+                    for (int readNum = 0; readNum < readouts->n; readNum++) {
+                        pmReadout *readout = readouts->data[readNum]; // The readout of interest
+                        if (! readout->weight) {
+                            psLogMsg(__func__, PS_LOG_WARN, "No weight image to write out in %d,%d,%d\n",
+                                     chipNum, cellNum, readNum);
+                        } else {
+                            if (! psFitsWriteImage(weightDest, header, readout->weight, readNum)) {
+                                psError(PS_ERR_IO, false, "Unable to write weight plane %d in extension %s\n",
+                                        readNum, hdu->extname);
+                                return false;
+                            }
+                        }
+                    } // Iterating over readouts
+                    psFree(header);
+                    psFree(weightDest);
+                } // Valid cells
+            } // Iterating over cells
+        } // Valid chips
+    } // Iterating over chips
+
+    return true;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFPAWrite.h	(revision 7462)
@@ -0,0 +1,20 @@
+#ifndef PM_FPA_WRITE_H
+#define PM_FPA_WRITE_H
+
+#include "pmFPA.h"
+
+bool pmFPAWrite(psFits *fits,		// FITS file to which to write
+		pmFPA *fpa,		// FPA to write
+		psDB *db		// Database to update
+    );
+
+bool pmFPAWriteMask(pmFPA *fpa, 	// FPA containing mask to write
+		    psFits *fits	// FITS file for image
+    );
+
+bool pmFPAWriteWeight(pmFPA *fpa, 	// FPA containing mask to write
+		      psFits *fits	// FITS file for image
+    );
+
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.c	(revision 7462)
@@ -0,0 +1,182 @@
+/** @file  pmFlatField.c
+ *
+ *  @brief Given an input image and a flat field image, pmFlatField shall divide the input image by the flat
+ *  field image.
+ *
+ *  The input image, in, and the flat field image, flat, need not be the same size, since the input image may
+ *  already have been trimmed (following overscan subtraction), but the function shall use the offsets in the
+ *  image (in->x0 and in->y0) to determine the appropriate offsets to obtain the correct pixel on the flat
+ *  field. In the event that the flat image is too small (i.e., pixels on the input image refer to pixels
+ *  outside the range of the flat image), the function shall generate an error. Pixels which are negative or
+ *  zero in the flat shall be masked in the input image with the value PM_MASK_FLAT. Negative pixels in the
+ *  flat may be set to zero so that they are treated identically to zeroes. Any pixels masked in the flat
+ *  shall be masked with corresponding values in the output. The function shall not normalize the flat; this
+ *  responsibility is left to the caller. This function is basically equivalent to a divide (with psImageOp),
+ *  but with care for the region that is divided, checking for negative pixels, and copying of the mask from
+ *  the flat to the output.
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-30 21:46:15 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include<stdio.h>
+#include<math.h>
+#include <strings.h>
+
+#include "pslib.h"
+#include "pmFlatField.h"
+#include "pmMaskBadPixels.h"
+#include "pmFlatFieldErrors.h"
+
+
+bool pmFlatField(pmReadout *in, pmReadout *mask, const pmReadout *flat)
+{
+    // XXX: Not sure if this is correct.  Must consult with IfA.
+    PS_ASSERT_PTR_NON_NULL(mask, false);
+    int i = 0;
+    int j = 0;
+    int totOffCol = 0;
+    int totOffRow = 0;
+    psElemType inType;
+    psElemType flatType;
+    psElemType maskType;
+    psImage *inImage = NULL;
+    psImage *inMask = NULL;
+    psImage *flatImage = NULL;
+
+
+    // Check for nulls
+    if (in == NULL) {
+        return true;       // Readout may not have data in it
+    } else if(flat==NULL) {
+        psError( PS_ERR_BAD_PARAMETER_NULL, true,
+                 PS_ERRORTEXT_pmFlatField_NULL_FLAT_READOUT);
+        return false;
+    }
+
+    inImage = in->image;
+    flatImage = flat->image;
+    if (inImage == NULL) {
+        psError( PS_ERR_BAD_PARAMETER_NULL, true,
+                 PS_ERRORTEXT_pmFlatField_NULL_INPUT_IMAGE);
+        return false;
+    } else if(flatImage == NULL) {
+        psError( PS_ERR_BAD_PARAMETER_NULL, true,
+                 PS_ERRORTEXT_pmFlatField_NULL_FLAT_IMAGE);
+        return false;
+    }
+    inMask = mask->image;
+
+    // Check input image and its mask are not larger than flat image
+
+    if (inImage->numRows>flatImage->numRows || inImage->numCols>flatImage->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmFlatField_SIZE_INPUT_IMAGE,
+                 inImage->numRows, inImage->numCols, flatImage->numRows, flatImage->numCols);
+        return false;
+    }
+    if (inMask->numRows > flatImage->numRows || inMask->numCols > flatImage->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmFlatField_SIZE_MASK_IMAGE,
+                 inMask->numRows, inMask->numCols, flatImage->numRows, flatImage->numCols);
+        return false;
+    }
+
+    // Determine total offset based on image offset with chip offset
+    totOffCol = inImage->col0 + in->col0;
+    totOffRow = inImage->row0 + in->row0;
+
+    // Check that offsets are within image limits
+    if(totOffRow>=flatImage->numRows || totOffCol>=flatImage->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmFlatField_OFFSET_FLAT_IMAGE,
+                 totOffRow, totOffCol, flatImage->numRows, flatImage->numCols);
+        return false;
+    } else if(totOffRow>=inImage->numRows || totOffCol>=inImage->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmFlatField_OFFSET_INPUT_IMAGE,
+                 totOffRow, totOffCol, inImage->numRows, inImage->numCols);
+        return false;
+    } else if(totOffRow>=inMask->numRows || totOffCol>=inMask->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmFlatField_OFFSET_MASK_IMAGE,
+                 totOffRow, totOffCol, inMask->numRows, inMask->numCols);
+        return false;
+    }
+
+    // Check for incorrect types
+    inType = inImage->type.type;
+    flatType = flatImage->type.type;
+    maskType = inMask->type.type;
+    if(PS_IS_PSELEMTYPE_COMPLEX(inType)) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmFlatField_TYPE_INPUT_IMAGE,
+                 inType);
+        return false;
+    } else if(PS_IS_PSELEMTYPE_COMPLEX(flatType)) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmFlatField_TYPE_FLAT_IMAGE,
+                 flatType);
+        return false;
+    } else if(maskType != PS_TYPE_MASK) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmFlatField_TYPE_MASK_IMAGE,
+                 maskType);
+        return false;
+    } else if(inType != flatType) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmFlatField_TYPE_MISMATCH,
+                 inType, flatType);
+        return false;
+    }
+
+    // Macro for all PS types
+    #define PM_FLAT_DIVISION(TYPE)                                                                           \
+case PS_TYPE_##TYPE:                                                                                         \
+    /* Per Eugene's request, use two sets of loops: first to fill mask, second to avoid div with bad pix */  \
+    for(j = totOffRow; j < inImage->numRows; j++) {                                                          \
+        for(i = totOffCol; i < inImage->numCols; i++) {                                                      \
+            if(flatImage->data.TYPE[j][i] <= 0.0) {                                                          \
+                /* Negative or zero flat pixels shall be masked in input image as  PM_MASK_FLAT */           \
+                inMask->data.PS_TYPE_MASK_DATA[j][i] |= PM_MASK_FLAT;                                        \
+                flatImage->data.TYPE[j][i] = 0.0;                                                            \
+            }                                                                                                \
+        }                                                                                                    \
+    }                                                                                                        \
+    for(j = totOffRow; j < inImage->numRows; j++) {                                                          \
+        for(i = totOffCol; i < inImage->numCols; i++) {                                                      \
+            if(!inMask->data.PS_TYPE_MASK_DATA[j][i]) {                                                      \
+                /* Module shall divide the input image by the flat-fielded image */                          \
+                inImage->data.TYPE[j][i] /= flatImage->data.TYPE[j][i];                                      \
+            }                                                                                                \
+        }                                                                                                    \
+    }                                                                                                        \
+    break;
+
+    switch(inType) {
+        PM_FLAT_DIVISION(U8);
+        PM_FLAT_DIVISION(U16);
+        PM_FLAT_DIVISION(U32);
+        PM_FLAT_DIVISION(U64);
+        PM_FLAT_DIVISION(S8);
+        PM_FLAT_DIVISION(S16);
+        PM_FLAT_DIVISION(S32);
+        PM_FLAT_DIVISION(S64);
+        PM_FLAT_DIVISION(F32);
+        PM_FLAT_DIVISION(F64);
+    default:
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmFlatField_TYPE_UNSUPPORTED,
+                 inType);
+    }
+
+    return true;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatField.h	(revision 7462)
@@ -0,0 +1,42 @@
+/** @file  pmFlatField.h
+ *
+ *  @brief Given an input image and a flat field image, pmFlatField shall divide the input image by the flat
+ *  field image.
+ *
+ *  The input image, in, and the flat field image, flat, need not be the same size, since the input image may
+ *  already have been trimmed (following overscan subtraction), but the function shall use the offsets in the
+ *  image (in->x0 and in->y0) to determine the appropriate offsets to obtain the correct pixel on the flat
+ *  field. In the event that the flat image is too small (i.e., pixels on the input image refer to pixels
+ *  outside the range of the flat image), the function shall generate an error. Pixels which are negative or
+ *  zero in the flat shall be masked in the input image with the value PM_MASK_FLAT. Negative pixels in the
+ *  flat may be set to zero so that they are treated identically to zeroes. Any pixels masked in the flat
+ *  shall be masked with corresponding values in the output. The function shall not normalize the flat; this
+ *  responsibility is left to the caller. This function is basically equivalent to a divide (with psImageOp),
+ *  but with care for the region that is divided, checking for negative pixels, and copying of the mask from
+ *  the flat to the output.
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-03 01:30:32 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "pslib.h"
+#include "pmFPA.h" // #include "pmAstrometry.h"
+
+
+/** Execute flat field module.
+ *
+ *  Given an input image and a flat-field image, pmFlatField shall divide the input image by the flat field
+ *  image.
+ *
+ *  @return  bool: True or false for success or failure
+ */
+bool pmFlatField(
+    pmReadout *in,          ///< Readout with input image
+    pmReadout *mask,        ///< Input image mask
+    const pmReadout *flat   ///< Readout with flat image
+);
+
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatFieldErrors.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatFieldErrors.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmFlatFieldErrors.h	(revision 7462)
@@ -0,0 +1,47 @@
+/** @file  pmFlatFieldErrors.h
+ *
+ *  @brief Contains the error text for the flat field module
+ *
+ *  @ingroup ErrorHandling
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-03 01:30:32 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_FLATFIELD_ERRORS_H
+#define PM_FLATFIELD_ERRORS_H
+
+/* N.B., lines between '//~Start' and '//~End' are automatic generated from
+ * the template following the '//~Start'.  The template is used to generate
+ * the other lines by, for each error text in psDataManipErrors.dat, the following
+ * substitutions are made:
+ *     $1  The error text macro name (first word in the psFlatFieldErrors.h lines)
+ *     $2  The error text (rest of the line in psFlatFieldErrors.h)
+ *     $n  The order of the source line in psFlatFieldErrors.h (comments excluded)
+ *
+ * DO NOT EDIT THE LINES BETWEEN //~Start and //~End!  ANY CHANGES WILL BE OVERWRITTEN.
+ */
+
+#define PS_ERRORNAME_DOMAIN "psModule.src."
+
+//~Start #define PS_ERRORTEXT_$1 "$2"
+#define PS_ERRORTEXT_pmFlatField_NULL_FLAT_READOUT "Null not allowed for flat readout."
+#define PS_ERRORTEXT_pmFlatField_NULL_INPUT_IMAGE "Null not allowed for input image."
+#define PS_ERRORTEXT_pmFlatField_NULL_FLAT_IMAGE "Null not allowed for flat image."
+#define PS_ERRORTEXT_pmFlatField_SIZE_INPUT_IMAGE "Input image size exceeds that of flat image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmFlatField_SIZE_MASK_IMAGE "Input image mask size exceeds that of flat image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmFlatField_OFFSET_FLAT_IMAGE "Total offset >= flat image size: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmFlatField_OFFSET_INPUT_IMAGE "Total offset >= input image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmFlatField_OFFSET_MASK_IMAGE "Total offset >= input image mask: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmFlatField_TYPE_INPUT_IMAGE "Complex types not allowed for input image. Type: %d"
+#define PS_ERRORTEXT_pmFlatField_TYPE_FLAT_IMAGE "Complex types not allowed for flat image. Type: %d"
+#define PS_ERRORTEXT_pmFlatField_TYPE_MASK_IMAGE "Mask must be PS_TYPE_MASK type. Type: %d"
+#define PS_ERRORTEXT_pmFlatField_TYPE_MISMATCH "Input and flat image types differ: (%d vs %d)"
+#define PS_ERRORTEXT_pmFlatField_TYPE_UNSUPPORTED "Unsupported image datatype. Type: %d"
+//~End
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.c	(revision 7462)
@@ -0,0 +1,183 @@
+/** @file  pmMaskBadPixels.c
+ *
+ *  @brief Given an input image, a bad pixel mask, a corresponding value in the bad pixel mask to mask, a
+ *  saturation level, and a growing radius, mask in the input image those pixels in the bad pixel mask that
+ *  match the value to mask.
+ *
+ *  Given an input image, in, a bad pixel mask, a corresponding value in the bad pixel mask to mask in the
+ * input image, maskVal, a saturation level, and a growing radius, pmMaskBadPixels shall mask in the input
+ * image those pixels in the bad pixel mask that match the value to mask. Note that the input image, in, is
+ * modified in-place. All pixels in the mask which satisfy the maskVal shall have their corresponding pixels
+ * masked in the input image, in. All pixels which satisfy the growVal shall have their corresponding
+ * pixels, along with all pixels within the grow radius masked. Pixels which have flux greater than sat shall
+ * also be masked, but not grown. Note that the input image, in, and the mask need not be the same size, since
+ * the input image may already have been trimmed (following overscan subtraction), but the function shall use
+ * the offsets in the image (in->x0 and in->y0) to determine the appropriate offsets to obtain the correct
+ * pixel on the mask. In the event that the mask image is too small (i.e., pixels on the input image refer to
+ * pixels outside the range of the mask image), the function shall generate an error.
+ 
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-30 21:46:15 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include<stdio.h>
+#include<math.h>
+#include<strings.h>
+
+#include "pmMaskBadPixels.h"
+#include "pmMaskBadPixelsErrors.h"
+
+bool pmMaskBadPixels(pmReadout *in, const psImage *mask, unsigned int maskVal, float sat,
+                     unsigned int growVal, int grow)
+{
+    int i = 0;
+    int j = 0;
+    int jj = 0;
+    int ii = 0;
+    int rRound = 0;
+    int rowMin = 0;
+    int rowMax = 0;
+    int colMin = 0;
+    int colMax = 0;
+    int totOffCol = 0;
+    int totOffRow = 0;
+    float r = 0.0f;
+    psElemType inType;
+    psElemType maskType;
+    psImage *inImage = NULL;
+    psImage *inMask = NULL;
+
+
+    // Check for nulls
+    if (in == NULL) {
+        return true;   // Readout may not have data in it
+    } else if(mask==NULL) {
+        psError( PS_ERR_BAD_PARAMETER_NULL, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_NULL_MASK_IMAGE);
+        return false;
+    }
+
+    inImage = in->image;
+    if (inImage == NULL) {
+        psError( PS_ERR_BAD_PARAMETER_NULL, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_NULL_INPUT_IMAGE);
+        return false;
+    } else if(in->mask == NULL) {
+        in->mask = psImageAlloc(inImage->numCols, inImage->numRows, PS_TYPE_MASK);
+        memset(in->mask->data.V[0], 0, inImage->numCols*inImage->numRows*PSELEMTYPE_SIZEOF(PS_TYPE_MASK));
+    }
+    inMask = in->mask;
+
+    // Check input image and its mask are not larger than mask
+    if(inImage->numRows > mask->numRows || inImage->numCols > mask->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_SIZE_INPUT_IMAGE,
+                 inImage->numRows, inImage->numCols, mask->numRows, mask->numCols);
+        return false;
+    } else if(inMask->numRows>mask->numRows || inMask->numCols>mask->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_SIZE_MASK_IMAGE,
+                 inMask->numRows, inMask->numCols, mask->numRows, mask->numCols);
+        return false;
+    }
+
+    // Determine total offset based on image offset with chip offset
+    totOffCol = inImage->col0 + in->col0;
+    totOffRow = inImage->row0 + in->row0;
+
+    // Check that offsets are within image limits
+    if(totOffRow>=mask->numRows || totOffCol>=mask->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_OFFSET_MASK_IMAGE,
+                 totOffRow, totOffCol, mask->numRows, mask->numCols);
+        return false;
+    } else if(totOffRow>=inImage->numRows || totOffCol>=inImage->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_OFFSET_INPUT_IMAGE,
+                 totOffRow, totOffCol, inImage->numRows, inImage->numCols);
+        return false;
+    } else if(totOffRow>=inMask->numRows || totOffCol>=inMask->numCols) {
+        psError( PS_ERR_BAD_PARAMETER_SIZE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_OFFSET_INPUT_IMAGE_MASK,
+                 totOffRow, totOffCol, inMask->numRows, inMask->numCols);
+        return false;
+    }
+
+    // Check for incorrect types
+    inType = inImage->type.type;
+    maskType = mask->type.type;
+    if(PS_IS_PSELEMTYPE_COMPLEX(inType)) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_TYPE_INPUT_IMAGE,
+                 inType);
+        return false;
+    } else if(maskType!=PS_TYPE_MASK) {
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_TYPE_MASK_IMAGE,
+                 maskType);
+        return false;
+    }
+
+    // Macro for all PS types
+    #define PM_BAD_PIXELS(TYPE)                                                                              \
+case PS_TYPE_##TYPE:                                                                                         \
+    for(j=totOffRow; j<inImage->numRows; j++) {                                                              \
+        for(i=totOffCol; i<inImage->numCols; i++) {                                                          \
+            \
+            /* Pixels with flux greater than sat shall be masked */                                          \
+            if(inImage->data.TYPE[j][i] > sat) {                                                             \
+                inMask->data.PS_TYPE_MASK_DATA[j][i] |= PM_MASK_SAT;                                         \
+            }                                                                                                \
+            \
+            /* Pixels which satisfy maskVal shall be masked */                                               \
+            inMask->data.PS_TYPE_MASK_DATA[j][i] |= (mask->data.PS_TYPE_MASK_DATA[j][i]&maskVal);            \
+            \
+            /* Pixels which satisfy growVal and within the grow radius shall be masked */                    \
+            if(mask->data.PS_TYPE_MASK_DATA[j][i] & growVal) {                                               \
+                rowMin = MAX(j-grow, 0);                                                                     \
+                rowMax = MIN(j+grow+1, inImage->numRows);                                                    \
+                colMin = MAX(i-grow, 0);                                                                     \
+                colMax = MIN(i+grow+1, inImage->numCols);                                                    \
+                for(jj=rowMin; jj<rowMax; jj++) {                                                            \
+                    for(ii=colMin; ii<colMax; ii++) {                                                        \
+                        r = sqrtf((ii-i)*(ii-i)+(jj-j)*(jj-j));                                              \
+                        rRound = r + 0.5;                                                                    \
+                        if(rRound <= grow) {                                                                 \
+                            inMask->data.PS_TYPE_MASK_DATA[jj][ii] |=                                        \
+                                    (mask->data.PS_TYPE_MASK_DATA[j][i]&growVal);                            \
+                        }                                                                                    \
+                    }                                                                                        \
+                }                                                                                            \
+            }                                                                                                \
+        }                                                                                                    \
+    }                                                                                                        \
+    break;
+
+    // Switch to call bad pixel masking macro defined above
+    switch(inType) {
+        PM_BAD_PIXELS(U8);
+        PM_BAD_PIXELS(U16);
+        PM_BAD_PIXELS(U32);
+        PM_BAD_PIXELS(U64);
+        PM_BAD_PIXELS(S8);
+        PM_BAD_PIXELS(S16);
+        PM_BAD_PIXELS(S32);
+        PM_BAD_PIXELS(S64);
+        PM_BAD_PIXELS(F32);
+        PM_BAD_PIXELS(F64);
+    default:
+        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
+                 PS_ERRORTEXT_pmMaskBadPixels_TYPE_UNSUPPORTED,
+                 inType);
+    }
+
+    return false;
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixels.h	(revision 7462)
@@ -0,0 +1,61 @@
+/** @file  pmMaskBadPixels.h
+ *
+ *  @brief Given an input image, a bad pixel mask, a corresponding value in the bad pixel mask to mask, a
+ *  saturation level, and a growing radius, mask in the input image those pixels in the bad pixel mask that
+ *  match the value to mask.
+ *
+ *  Given an input image, in, a bad pixel mask, a corresponding value in the bad pixel mask to mask in the
+ *  input image, maskVal, a saturation level, and a growing radius, pmMaskBadPixels shall mask in the input
+ *  image those pixels in the bad pixel mask that match the value to mask. Note that the input image, in, is
+ *  modified in-place. All pixels in the mask which satisfy the maskVal shall have their corresponding pixels
+ *  masked in the input image, in. All pixels which satisfy the growVal shall have their corresponding
+ *  pixels, along with all pixels within the grow radius masked. Pixels which have flux greater than sat shall
+ *  also be masked, but not grown. Note that the input image, in, and the mask need not be the same size,
+ *  since the input image may already have been trimmed (following overscan subtraction), but the function
+ *  shall use the offsets in the image (in->x0 and in->y0) to determine the appropriate offsets to obtain the
+ *  correct pixel on the mask. In the event that the mask image is too small (i.e., pixels on the input image
+ *  refer to pixels outside the range of the mask image), the function shall generate an error.
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-09-23 02:58:30 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+/** Mask values */
+typedef enum {
+    PM_MASK_TRAP    = 0x0001,   ///< The pixel is a charge trap.
+    PM_MASK_BADCOL  = 0x0002,   ///< The pixel is a bad column.
+    PM_MASK_SAT     = 0x0004,   ///< The pixel is saturated.
+    PM_MASK_FLAT    = 0x0008    ///< The pixel is non-positive in the flat-field.
+} pmMaskValue;
+
+/** Macro to find maximum of two numbers */
+#define MAX(A,B)((A)>=(B)?(A):(B))
+
+/** Macro to find minimum of two numbers */
+#define MIN(A,B)((A)<=(B)?(A):(B))
+
+
+/** Execute bad pixels module.
+ *
+ *  Given an input image, a bad pixel mask, a corresponding value in the bad pixel mask to mask, a
+ *  saturation level, and a growing radius, mask in the input image those pixels in the bad pixel mask that
+ *  match the value to mask.
+ *
+ *  @return  bool: True or false for success or failure
+ */
+bool pmMaskBadPixels(
+    pmReadout *in,          ///< Readout containing input image data.
+    const psImage *mask,    ///< Mask data to be added to readout mask data.
+    unsigned int maskVal,   ///< Mask value to determine what to add to input mask.
+    float sat,              ///< Saturation limit to mask bad pixels.
+    unsigned int growVal,   ///< Mask data to determine if a circurlar area should be masked.
+    int grow                ///< Radius of mask to apply around pixel.
+);
+
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixelsErrors.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixelsErrors.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmMaskBadPixelsErrors.h	(revision 7462)
@@ -0,0 +1,45 @@
+/** @file  pmMaskBadPixelsErrors.h
+ *
+ *  @brief Contains the error text for the mask bad pixels module
+ *
+ *  @ingroup ErrorHandling
+ *
+ *  @author Ross Harman, MHPCC
+ *
+ *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-09-23 02:58:30 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_FLATFIELD_ERRORS_H
+#define PM_FLATFIELD_ERRORS_H
+
+/* N.B., lines between '//~Start' and '//~End' are automatic generated from
+ * the template following the '//~Start'.  The template is used to generate
+ * the other lines by, for each error text in pmMaskBadPixelsErrors.dat, the following
+ * substitutions are made:
+ *     $1  The error text macro name (first word in the pmMaskBadPixelsErrors.h lines)
+ *     $2  The error text (rest of the line in pmMaskBadPixelsErrors.h)
+ *     $n  The order of the source line in pmMaskBadPixelsErrors.h (comments excluded)
+ * 
+ * DO NOT EDIT THE LINES BETWEEN //~Start and //~End!  ANY CHANGES WILL BE OVERWRITTEN.
+ */
+
+#define PS_ERRORNAME_DOMAIN "psModule.src."
+
+//~Start #define PS_ERRORTEXT_$1 "$2"
+#define PS_ERRORTEXT_pmMaskBadPixels_NULL_MASK_IMAGE "Null not allowed for mask image."
+#define PS_ERRORTEXT_pmMaskBadPixels_NULL_INPUT_IMAGE "Null not allowed for input image."
+#define PS_ERRORTEXT_pmMaskBadPixels_SIZE_INPUT_IMAGE "Input image size exceeds that of mask image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_SIZE_MASK_IMAGE "Input image mask size exceeds that of mask image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_OFFSET_MASK_IMAGE "Total offset >= mask image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_OFFSET_INPUT_IMAGE "Total offset >= input image: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_OFFSET_INPUT_IMAGE_MASK "Total offset >= input image mask: (%d, %d) vs (%d, %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_TYPE_INPUT_IMAGE "Complex types not allowed for input image. Type: %d"
+#define PS_ERRORTEXT_pmMaskBadPixels_TYPE_MASK_IMAGE "Mask must be PS_TYPE_MASK type. Type: %d"
+#define PS_ERRORTEXT_pmMaskBadPixels_TYPE_MISMATCH "Input and flat image types differ: (%d vs %d)"
+#define PS_ERRORTEXT_pmMaskBadPixels_TYPE_UNSUPPORTED "Unsupported image datatype. Type: %d"
+//~End
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.c	(revision 7462)
@@ -0,0 +1,123 @@
+/** @file  pmNonLinear.c
+ *
+ *  Provides polynomial or table lookup non-linearity corrections to readouts.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-03 03:21:36 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ *  XXX: The SDR is silent about image types.  Only F32 was implemented.
+ *
+ */
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include<stdio.h>
+#include<math.h>
+
+#include "pmNonLinear.h"
+
+/******************************************************************************
+pmNonLinearityLookup(): This routine will take an pmReadout image as input
+and a 1-D polynomial.  For each pixel in the input image, the polynomial will
+be evaluated at that pixels value, and the image pixel will then be set to
+that value.
+ *****************************************************************************/
+
+pmReadout *pmNonLinearityPolynomial(pmReadout *inputReadout,
+                                    const psPolynomial1D *input1DPoly)
+{
+    PS_ASSERT_PTR_NON_NULL(inputReadout, NULL);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image, NULL);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, NULL);
+    PS_ASSERT_PTR_NON_NULL(input1DPoly, NULL);
+
+    psS32 i;
+    psS32 j;
+
+    for (i=0;i<inputReadout->image->numRows;i++) {
+        for (j=0;j<inputReadout->image->numCols;j++) {
+            inputReadout->image->data.F32[i][j] = psPolynomial1DEval(input1DPoly, inputReadout->image->data.F32[i][j]);
+        }
+    }
+    return(inputReadout);
+}
+
+
+/******************************************************************************
+pmNonLinearityLookup(): This routine will take an pmReadout image as input
+and two input vectors, which constitute a lookup table.  For each pixel in
+the input image, that pixels value will be determined in the input vector
+inFluxe, and the corresponding value in outFlux.  The image pixel will then
+be set to the value from outFlux.
+ *****************************************************************************/
+pmReadout *pmNonLinearityLookup(pmReadout *inputReadout,
+                                const psVector *inFlux,
+                                const psVector *outFlux)
+{
+    PS_ASSERT_PTR_NON_NULL(inputReadout,NULL);
+    PS_ASSERT_PTR_NON_NULL(inputReadout->image,NULL);
+    PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, NULL);
+    PS_ASSERT_PTR_NON_NULL(inFlux,NULL);
+    if (inFlux->n < 2) {
+        psError(PS_ERR_UNKNOWN,true, "pmNonLinearityLookup(): input vector less than 2 elements.  Returning inputReadout image.");
+        return(inputReadout);
+    }
+    PS_ASSERT_PTR_NON_NULL(outFlux,NULL);
+    psS32 tableSize = inFlux->n;
+    if (inFlux->n != outFlux->n) {
+        tableSize = PS_MIN(inFlux->n, outFlux->n);
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmNonLinear.c: pmNonLinearityLookup(): input vectors have different sizes (%d, %d)\n", inFlux->n, outFlux->n);
+    }
+    PS_ASSERT_VECTOR_TYPE(inFlux, PS_TYPE_F32, NULL);
+    PS_ASSERT_VECTOR_TYPE(outFlux, PS_TYPE_F32, NULL);
+
+    psS32 i;
+    psS32 j;
+    psS32 binNum;
+    psScalar x;
+    psS32 numPixels = 0;
+    psF32 slope;
+
+    x.type.type = PS_TYPE_F32;
+    for (i=0;i<inputReadout->image->numRows;i++) {
+        for (j=0;j<inputReadout->image->numCols;j++) {
+            x.data.F32 = inputReadout->image->data.F32[i][j];
+            binNum = p_psVectorBinDisect((psVector *)inFlux, &x);
+
+            if (binNum == -2) {
+                // We get here if x is below the table lookup range.
+                inputReadout->image->data.F32[i][j] = outFlux->data.F32[0];
+                numPixels++;
+
+            } else if (binNum == -1) {
+                // We get here if x is above the table lookup range.
+                inputReadout->image->data.F32[i][j] = outFlux->data.F32[tableSize-1];
+                numPixels++;
+
+            } else if (binNum < -2) {
+                // We get here if there was some other problem.
+                psError(PS_ERR_UNKNOWN,true, "pmNonLinearityLookup(): Could not perform p_psVectorBinDisect().  Returning inputReadout image.");
+                return(inputReadout);
+                numPixels++;
+            } else {
+                // Perform linear interpolation.
+                slope = (outFlux->data.F32[binNum+1] - outFlux->data.F32[binNum]) /
+                        (inFlux->data.F32[binNum+1]  - inFlux->data.F32[binNum]);
+                inputReadout->image->data.F32[i][j] = outFlux->data.F32[binNum] +
+                                                      ((x.data.F32 - inFlux->data.F32[binNum]) * slope);
+            }
+        }
+    }
+    if (numPixels > 0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmNonLinear.c: pmNonLinearityLookup(): %d pixels outside table.", numPixels);
+    }
+    return(inputReadout);
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmNonLinear.h	(revision 7462)
@@ -0,0 +1,27 @@
+/** @file  pmNonLinear.h
+ *
+ *  Provides polynomial or table lookup non-linearity corrections to readouts.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-03 03:21:36 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#if !defined(PM_NON_LINEAR_H)
+#define PM_NON_LINEAR_H
+
+#include "pslib.h"
+#include "pmFPA.h" // #include "pmAstrometry.h"
+
+pmReadout *pmNonLinearityPolynomial(pmReadout *in,
+                                    const psPolynomial1D *coeff);
+
+pmReadout *pmNonLinearityLookup(pmReadout *in,
+                                const psVector *inFlux,
+                                const psVector *outFlux);
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.c	(revision 7462)
@@ -0,0 +1,685 @@
+/** @file  pmSubtractBias.c
+ *
+ *  This file will contain a module which will subtract the detector bias
+ *  in place from an input image.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-12-14 03:47:35 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pmSubtractBias.h"
+
+#define PM_SUBTRACT_BIAS_POLYNOMIAL_ORDER 2
+#define PM_SUBTRACT_BIAS_SPLINE_ORDER 3
+
+// XXX: put these in psConstants.h
+void PS_POLY1D_PRINT(psPolynomial1D *poly)
+{
+    printf("-------------- PS_POLY1D_PRINT() --------------\n");
+    printf("poly->nX is %d\n", poly->nX);
+    for (psS32 i = 0 ; i < (1 + poly->nX) ; i++) {
+        printf("poly->coeff[%d] is %f\n", i, poly->coeff[i]);
+    }
+}
+
+void PS_PRINT_SPLINE(psSpline1D *mySpline)
+{
+    printf("-------------- PS_PRINT_SPLINE() --------------\n");
+    printf("mySpline->n is %d\n", mySpline->n);
+    for (psS32 i = 0 ; i < mySpline->n ; i++) {
+        PS_POLY1D_PRINT(mySpline->spline[i]);
+    }
+    PS_VECTOR_PRINT_F32(mySpline->knots);
+}
+
+#define PS_IMAGE_PRINT_F32_HIDEF(NAME) \
+printf("======== printing %s ========\n", #NAME); \
+for (int i = 0 ; i < (NAME)->numRows ; i++) { \
+    for (int j = 0 ; j < (NAME)->numCols ; j++) { \
+        printf("%.5f ", (NAME)->data.F32[i][j]); \
+    } \
+    printf("\n"); \
+}\
+
+/******************************************************************************
+psSubtractFrame(): this routine will take as input a readout for the input
+image and a readout for the bias image.  The bias image is subtracted in
+place from the input image.
+*****************************************************************************/
+static pmReadout *SubtractFrame(pmReadout *in,
+                                const pmReadout *bias)
+{
+    psS32 i;
+    psS32 j;
+
+    if (bias == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractBias.c: SubtractFrame(): bias frame is NULL.  Returning original image.\n");
+        return(in);
+    }
+
+
+    if ((in->image->numRows + in->row0 - bias->row0) > bias->image->numRows) {
+        psError(PS_ERR_UNKNOWN,true, "bias image does not have enough rows.  Returning in image\n");
+        return(in);
+    }
+    if ((in->image->numCols + in->col0 - bias->col0) > bias->image->numCols) {
+        psError(PS_ERR_UNKNOWN,true, "bias image does not have enough columns.  Returning in image\n");
+        return(in);
+    }
+
+    for (i=0;i<in->image->numRows;i++) {
+        for (j=0;j<in->image->numCols;j++) {
+            in->image->data.F32[i][j]-=
+                bias->image->data.F32[i+in->row0-bias->row0][j+in->col0-bias->col0];
+            if ((in->mask != NULL) && (bias->mask != NULL)) {
+                (in->mask->data.U8[i][j])|=
+                    bias->mask->data.U8[i+in->row0-bias->row0][j+in->col0-bias->col0];
+            }
+        }
+    }
+
+    return(in);
+}
+
+/******************************************************************************
+ImageSubtractScalar(): subtract a scalar from the input image.
+ 
+XXX: Use a psLib function for this.
+ 
+XXX: This should
+ *****************************************************************************/
+static psImage *ImageSubtractScalar(psImage *image,
+                                    psF32 scalar)
+{
+    for (psS32 i=0;i<image->numRows;i++) {
+        for (psS32 j=0;j<image->numCols;j++) {
+            image->data.F32[i][j]-= scalar;
+        }
+    }
+    return(image);
+}
+
+/******************************************************************************
+GenNewStatOptions(): this routine will take as input the options member of the
+stat data structure, determine if multiple options have been specified, issue
+a warning message if so, and return the highest priority option (according to
+the order of the if-statements in this code).  The higher priority options are
+listed lower in the code.
+ *****************************************************************************/
+static psStatsOptions GenNewStatOptions(const psStats *stat)
+{
+    psS32 numOptions = 0;
+    psStatsOptions opt = 0;
+
+    if (stat->options & PS_STAT_ROBUST_MODE) {
+        if (numOptions == 0) {
+            opt = PS_STAT_ROBUST_MODE;
+        }
+        numOptions++;
+    }
+
+    if (stat->options & PS_STAT_ROBUST_MEDIAN) {
+        if (numOptions == 0) {
+            opt = PS_STAT_ROBUST_MEDIAN;
+        }
+        numOptions++;
+    }
+
+    if (stat->options & PS_STAT_ROBUST_MEAN) {
+        if (numOptions == 0) {
+            opt = PS_STAT_ROBUST_MEAN;
+        }
+        numOptions++;
+    }
+
+    if (stat->options & PS_STAT_CLIPPED_MEAN) {
+        if (numOptions == 0) {
+            opt = PS_STAT_CLIPPED_MEAN;
+        }
+        numOptions++;
+    }
+
+    if (stat->options & PS_STAT_SAMPLE_MEDIAN) {
+        if (numOptions == 0) {
+            opt = PS_STAT_SAMPLE_MEDIAN;
+        }
+        numOptions++;
+    }
+
+    if (stat->options & PS_STAT_SAMPLE_MEAN) {
+        numOptions++;
+        opt = PS_STAT_SAMPLE_MEAN;
+    }
+
+
+    if (numOptions == 0) {
+        psError(PS_ERR_UNKNOWN,true, "No statistics options have been specified.\n");
+    }
+    if (numOptions != 1) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractBias.c: GenNewStatOptions(): Too many statistics options have been specified\n");
+    }
+    return(opt);
+}
+
+
+
+/******************************************************************************
+ScaleOverscanVector(): this routine takes as input an arbitrary vector,
+creates a new vector of length n, and fills the new vector with the
+interpolated values of the old vector.  The type of interpolation is:
+    PM_FIT_POLYNOMIAL: fit a polynomial to the entire input vector data.
+    PM_FIT_SPLINE: fit splines to the input vector data.
+XXX: Doesn't it make more sense to do polynomial interpolation on a few
+elements of the input vector, rather than fit a polynomial to the entire
+vector?
+ *****************************************************************************/
+static psVector *ScaleOverscanVector(psVector *overscanVector,
+                                     psS32 n,
+                                     void *fitSpec,
+                                     pmFit fit)
+{
+    psTrace(".psModule.pmSubtracBias.ScaleOverscanVector", 4,
+            "---- ScaleOverscanVector() begin (%d -> %d) ----\n", overscanVector->n, n);
+    //    PS_VECTOR_PRINT_F32(overscanVector);
+
+    if (NULL == overscanVector) {
+        return(overscanVector);
+    }
+
+    // Allocate the new vector.
+    psVector *newVec = psVectorAlloc(n, PS_TYPE_F32);
+
+    //
+    // If the new vector is the same size as the old, simply copy the data.
+    //
+    if (n == overscanVector->n) {
+        for (psS32 i = 0 ; i < n ; i++) {
+            newVec->data.F32[i] = overscanVector->data.F32[i];
+        }
+        return(newVec);
+    }
+    psPolynomial1D *myPoly;
+    psSpline1D *mySpline;
+    psF32 x;
+    psS32 i;
+    if (fit == PM_FIT_POLYNOMIAL) {
+        // Fit a polynomial to the old overscan vector.
+        myPoly = (psPolynomial1D *) fitSpec;
+        PS_ASSERT_POLY_NON_NULL(myPoly, NULL);
+        myPoly = psVectorFitPolynomial1D(myPoly, NULL, 0, overscanVector, NULL, NULL);
+        if (myPoly == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "ScaleOverscanVector()(1): Could not fit a polynomial to the psVector.\n");
+            return(NULL);
+        }
+
+        // For each element of the new vector, convert the x-ordinate to that
+        // of the old vector, use the fitted polynomial to determine the
+        // interpolated value at that point, and set the new vector.
+        for (i=0;i<n;i++) {
+            x = ((psF32) i) * ((psF32) overscanVector->n) / ((psF32) n);
+            newVec->data.F32[i] = psPolynomial1DEval(myPoly, x);
+        }
+    } else if (fit == PM_FIT_SPLINE) {
+        psS32 mustFreeSpline = 0;
+        // Fit a spline to the old overscan vector.
+        mySpline = (psSpline1D *) fitSpec;
+        // XXX: Does it make any sense to have a psSpline argument?
+        if (mySpline == NULL) {
+            mustFreeSpline = 1;
+        }
+
+        //
+        // NOTE: Since the X arg in the psVectorFitSpline1D() function is NULL,
+        // splines endpoints will be from 0.0 to overscanVector->n-1.  Must scale
+        // properly when doing the spline eval.
+        //
+        //        mySpline = psVectorFitSpline1D(mySpline, NULL, overscanVector, NULL);
+        mySpline = psVectorFitSpline1D(NULL, overscanVector);
+        if (mySpline == NULL) {
+            psError(PS_ERR_UNKNOWN, false, "ScaleOverscanVector()(2): Could not fit a spline to the psVector.\n");
+            return(NULL);
+        }
+        //        PS_PRINT_SPLINE(mySpline);
+
+        // For each element of the new vector, convert the x-ordinate to that
+        // of the old vector, use the fitted polynomial to determine the
+        // interpolated value at that point, and set the new vector.
+        for (i=0;i<n;i++) {
+            // Scale to [0 : overscanVector->n - 1]
+            x = ((psF32) i) * ((psF32) (overscanVector->n-1)) / ((psF32) n);
+            newVec->data.F32[i] = psSpline1DEval(mySpline, x);
+        }
+        if (mustFreeSpline ==1) {
+            psFree(mySpline);
+        }
+        //        PS_VECTOR_PRINT_F32(newVec);
+
+
+    } else {
+        psError(PS_ERR_UNKNOWN, true, "unknown fit type.  Returning NULL.\n");
+        psFree(newVec);
+        return(NULL);
+    }
+
+    psTrace(".psModule.pmSubtracBias.ScaleOverscanVector", 4,
+            "---- ScaleOverscanVector() exit ----\n");
+    return(newVec);
+}
+
+/******************************************************************************
+XXX: The SDRS does not specify type support.  F32 is implemented here.
+ *****************************************************************************/
+pmReadout *pmSubtractBias(pmReadout *in,
+                          void *fitSpec,
+                          const psList *overscans,
+                          pmOverscanAxis overScanAxis,
+                          psStats *stat,
+                          psS32 nBinOrig,
+                          pmFit fit,
+                          const pmReadout *bias)
+{
+    psTrace(".psModule.pmSubtracBias.pmSubtractBias", 4,
+            "---- pmSubtractBias() begin ----\n");
+    PS_ASSERT_READOUT_NON_NULL(in, NULL);
+    PS_ASSERT_READOUT_NON_EMPTY(in, NULL);
+    PS_ASSERT_READOUT_TYPE(in, PS_TYPE_F32, NULL);
+
+    //
+    // If the overscans != NULL, then check the type of each image.
+    //
+    if (overscans != NULL) {
+        psListElem *tmpOverscan = (psListElem *) overscans->head;
+        while (NULL != tmpOverscan) {
+            psImage *myOverscanImage = (psImage *) tmpOverscan->data;
+            PS_ASSERT_IMAGE_TYPE(myOverscanImage, PS_TYPE_F32, NULL);
+            tmpOverscan = tmpOverscan->next;
+        }
+    }
+
+    if ((overscans == NULL) && (overScanAxis != PM_OVERSCAN_NONE)) {
+        psError(PS_ERR_UNKNOWN,true, "(overscans == NULL) && (overScanAxis != PM_OVERSCAN_NONE).  Returning in image\n");
+        return(in);
+    }
+
+    // Check for an unallowable pmFit.
+    if ((fit != PM_OVERSCAN_NONE) &&
+            (fit != PM_OVERSCAN_ROWS) &&
+            (fit != PM_OVERSCAN_COLUMNS) &&
+            (fit != PM_OVERSCAN_ALL)) {
+        psError(PS_ERR_UNKNOWN, true, "fit is unallowable (%d).  Returning in image.\n", fit);
+        return(in);
+    }
+    // Check for an unallowable pmOverscanAxis.
+    if ((overScanAxis != PM_OVERSCAN_NONE) &&
+            (overScanAxis != PM_OVERSCAN_ROWS) &&
+            (overScanAxis != PM_OVERSCAN_COLUMNS) &&
+            (overScanAxis != PM_OVERSCAN_ALL)) {
+        psError(PS_ERR_UNKNOWN, true, "overScanAxis is unallowable (%d).  Returning in image.\n", overScanAxis);
+        return(in);
+    }
+    psS32 i;
+    psS32 j;
+    psS32 numBins = 0;
+    static psVector *overscanVector = NULL;
+    psVector *tmpRow = NULL;
+    psVector *tmpCol = NULL;
+    psVector *myBin = NULL;
+    psVector *binVec = NULL;
+    psListElem *tmpOverscan = NULL;
+    double statValue;
+    psImage *myOverscanImage = NULL;
+    psPolynomial1D *myPoly = NULL;
+    psSpline1D *mySpline = NULL;
+    psS32 nBin;
+
+    //
+    //  Create a static stats data structure and determine the highest
+    //  priority stats option.
+    //
+    static psStats *myStats = NULL;
+    if (myStats == NULL) {
+        myStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+        p_psMemSetPersistent(myStats, true);
+    }
+    if (stat != NULL) {
+        myStats->options = GenNewStatOptions(stat);
+    }
+
+
+    if (overScanAxis == PM_OVERSCAN_NONE) {
+        if (fit != PM_FIT_NONE) {
+            psLogMsg(__func__, PS_LOG_WARN,
+                     "WARNING: pmSubtractBias.(): overScanAxis equals NONE, and fit does not equal NONE.  Proceeding to full fram subtraction.\n");
+        }
+
+        if (overscans != NULL) {
+            psLogMsg(__func__, PS_LOG_WARN,
+                     "WARNING: pmSubtractBias.(): overScanAxis equals NONE and overscans does not equal NULL.  Proceeding to full fram subtraction.\n");
+        }
+        return(SubtractFrame(in, bias));
+    }
+
+    if ((overScanAxis == PM_OVERSCAN_ALL) && (fit != PM_FIT_NONE)) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmSubtractBias.(): overScanAxis equals ALL, and fit does not equal NONE.  Proceeding with the rest of the module.\n");
+    }
+
+
+    //
+    // We subtract each overscan region from the image data.
+    // If we get here we know that overscans != NULL.
+    //
+
+    if (overScanAxis == PM_OVERSCAN_ALL) {
+        tmpOverscan = (psListElem *) overscans->head;
+        while (NULL != tmpOverscan) {
+            myOverscanImage = (psImage *) tmpOverscan->data;
+
+            PS_ASSERT_IMAGE_TYPE(myOverscanImage, PS_TYPE_F32, NULL);
+            psStats *rc = psImageStats(myStats, myOverscanImage, NULL, (psMaskType)0xffffffff);
+            if (rc == NULL) {
+                psError(PS_ERR_UNKNOWN, false, "psImageStats(): could not perform requested statistical operation.  Returning in image.\n");
+                return(in);
+            }
+            if (false == p_psGetStatValue(myStats, &statValue)) {
+                psError(PS_ERR_UNKNOWN, false, "p_psGetStatValue(): could not determine result from requested statistical operation.  Returning in image.\n");
+                return(in);
+            }
+            ImageSubtractScalar(in->image, statValue);
+
+            tmpOverscan = tmpOverscan->next;
+        }
+        return(in);
+    }
+
+    // This check is redundant with above code.
+    if (!((overScanAxis == PM_OVERSCAN_ROWS) || (overScanAxis == PM_OVERSCAN_COLUMNS))) {
+        psError(PS_ERR_UNKNOWN, true, "overScanAxis is unallowable (%d).\nReturning in image.\n", overScanAxis);
+        return(in);
+    }
+
+    tmpOverscan = (psListElem *) overscans->head;
+    while (NULL != tmpOverscan) {
+        //        PS_IMAGE_PRINT_F32_HIDEF(in->image);
+        myOverscanImage = (psImage *) tmpOverscan->data;
+
+        if (overScanAxis == PM_OVERSCAN_ROWS) {
+            if (myOverscanImage->numCols != (in->image)->numCols) {
+                psLogMsg(__func__, PS_LOG_WARN,
+                         "WARNING: pmSubtractBias.(): overscan image has %d columns, input image has %d columns\n",
+                         myOverscanImage->numCols, in->image->numCols);
+            }
+
+            // We create a row vector and subtract this vector from image.
+            // XXX: Is there a better way to extract a psVector from a psImage without
+            // having to copy every element in that vector?
+            overscanVector = psVectorAlloc(myOverscanImage->numCols, PS_TYPE_F32);
+            for (i=0;i<overscanVector->n;i++) {
+                overscanVector->data.F32[i] = 0.0;
+            }
+            tmpRow = psVectorAlloc(myOverscanImage->numRows, PS_TYPE_F32);
+
+            // For each column of the input image, loop through every row,
+            // collect the pixel in that row, then performed the specified
+            // statistical op on those pixels.  Store this in overscanVector.
+            for (i=0;i<myOverscanImage->numCols;i++) {
+                for (j=0;j<myOverscanImage->numRows;j++) {
+                    tmpRow->data.F32[j] = myOverscanImage->data.F32[j][i];
+                }
+                psStats *rc = psVectorStats(myStats, tmpRow, NULL, NULL, 0);
+                if (rc == NULL) {
+                    psError(PS_ERR_UNKNOWN, false, "psVectorStats(): could not perform requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                if (false ==  p_psGetStatValue(rc, &statValue)) {
+                    psError(PS_ERR_UNKNOWN, false, "p_psGetStatValue(): could not determine result from requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                overscanVector->data.F32[i] = statValue;
+            }
+            psFree(tmpRow);
+
+            // Scale the overscan vector to the size of the input image.
+            if (overscanVector->n != in->image->numCols) {
+                if ((fit == PM_FIT_POLYNOMIAL) || (fit == PM_FIT_SPLINE)) {
+                    psVector *newVec = ScaleOverscanVector(overscanVector,
+                                                           in->image->numCols,
+                                                           fitSpec, fit);
+                    if (newVec == NULL) {
+                        psError(PS_ERR_UNKNOWN, false, "ScaleOverscanVector(): could not scale the overscan vector.  Returning in image.\n");
+                        return(in);
+                    }
+                    psFree(overscanVector);
+                    overscanVector = newVec;
+                } else {
+                    psError(PS_ERR_UNKNOWN, true, "Don't know how to scale the overscan vector.  Set fit to PM_FIT_SPLINE or PM_FIT_POLYNOMIAL.  Returning in image.\n");
+                    psFree(overscanVector);
+                    return(in);
+                }
+            }
+        }
+
+        if (overScanAxis == PM_OVERSCAN_COLUMNS) {
+            if (myOverscanImage->numRows != (in->image)->numRows) {
+                psLogMsg(__func__, PS_LOG_WARN,
+                         "WARNING: pmSubtractBias.(): overscan image has %d rows, input image has %d rows\n",
+                         myOverscanImage->numRows, in->image->numRows);
+            }
+
+            // We create a column vector and subtract this vector from image.
+            overscanVector = psVectorAlloc(myOverscanImage->numRows, PS_TYPE_F32);
+            for (i=0;i<overscanVector->n;i++) {
+                overscanVector->data.F32[i] = 0.0;
+            }
+            tmpCol = psVectorAlloc(myOverscanImage->numCols, PS_TYPE_F32);
+
+            // For each row of the input image, loop through every column,
+            // collect the pixel in that row, then performed the specified
+            // statistical op on those pixels.  Store this in overscanVector.
+            for (i=0;i<myOverscanImage->numRows;i++) {
+                for (j=0;j<myOverscanImage->numCols;j++) {
+                    tmpCol->data.F32[j] = myOverscanImage->data.F32[i][j];
+                }
+                psStats *rc = psVectorStats(myStats, tmpCol, NULL, NULL, 0);
+                if (rc == NULL) {
+                    psError(PS_ERR_UNKNOWN, false, "psVectorStats(): could not perform requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                if (false ==  p_psGetStatValue(rc, &statValue)) {
+                    psError(PS_ERR_UNKNOWN, false, "p_psGetStatValue(): could not determine result from requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                overscanVector->data.F32[i] = statValue;
+            }
+            psFree(tmpCol);
+
+            // Scale the overscan vector to the size of the input image.
+            if (overscanVector->n != in->image->numRows) {
+                if ((fit == PM_FIT_POLYNOMIAL) || (fit == PM_FIT_SPLINE)) {
+                    psVector *newVec = ScaleOverscanVector(overscanVector,
+                                                           in->image->numRows,
+                                                           fitSpec, fit);
+                    if (newVec == NULL) {
+                        psError(PS_ERR_UNKNOWN, false, "ScaleOverscanVector(): could not scale the overscan vector.  Returning in image.\n");
+                        return(in);
+                    }
+                    psFree(overscanVector);
+                    overscanVector = newVec;
+                } else {
+                    psError(PS_ERR_UNKNOWN, true, "Don't know how to scale the overscan vector.  Set fit to PM_FIT_SPLINE or PM_FIT_POLYNOMIAL.  Returning in image.\n");
+                    psFree(overscanVector);
+                    return(in);
+                }
+            }
+        }
+
+        //
+        // Re-bin the overscan vector (change its length).
+        //
+        // Only if nBinOrig > 1.
+        if ((nBinOrig > 1) && (nBinOrig < overscanVector->n)) {
+            numBins = 1+((overscanVector->n)/nBinOrig);
+            myBin = psVectorAlloc(numBins, PS_TYPE_F32);
+            binVec = psVectorAlloc(nBinOrig, PS_TYPE_F32);
+
+            for (i=0;i<numBins;i++) {
+                for(j=0;j<nBinOrig;j++) {
+                    if (overscanVector->n > ((i*nBinOrig)+j)) {
+                        binVec->data.F32[j] = overscanVector->data.F32[(i*nBinOrig)+j];
+                    } else {
+                        // XXX: we get here if nBinOrig does not evenly divide
+                        // the overscanVector vector.  This is the last bin.  Should
+                        // we change the binVec->n to acknowledge that?
+                        binVec->n = j;
+                    }
+                }
+                psStats *rc = psVectorStats(myStats, binVec, NULL, NULL, 0);
+                if (rc == NULL) {
+                    psError(PS_ERR_UNKNOWN, false, "psVectorStats(): could not perform requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                if (false ==  p_psGetStatValue(rc, &statValue)) {
+                    psError(PS_ERR_UNKNOWN, false, "p_psGetStatValue(): could not determine result from requested statistical operation.  Returning in image.\n");
+                    return(in);
+                }
+                myBin->data.F32[i] = statValue;
+            }
+
+            // Change the effective size of overscanVector.
+            overscanVector->n = numBins;
+            for (i=0;i<numBins;i++) {
+                overscanVector->data.F32[i] = myBin->data.F32[i];
+            }
+            psFree(binVec);
+            psFree(myBin);
+            nBin = nBinOrig;
+        } else {
+            nBin = 1;
+        }
+
+        // At this point the number of data points in overscanVector should be
+        // equal to the number of rows/columns (whatever is appropriate) in the
+        // image divided by numBins.
+        //
+
+
+        //
+        // This doesn't seem right.  The only way to do a spline fit is if,
+        // by SDRS requirements, fitSpec is not-NULL>  But in order for it
+        // to be non-NULL, someone must have called psSpline1DAlloc() with
+        // the min, max, and number of splines.
+        //
+        if (!((fitSpec == NULL) || (fit == PM_FIT_NONE))) {
+            //
+            // Fit a polynomial or spline to the overscan vector.
+            //
+            if (fit == PM_FIT_POLYNOMIAL) {
+                myPoly = (psPolynomial1D *) fitSpec;
+                myPoly = psVectorFitPolynomial1D(myPoly, NULL, 0, overscanVector, NULL, NULL);
+                if (myPoly == NULL) {
+                    psError(PS_ERR_UNKNOWN, false, "(3) Could not fit a polynomial to overscan vector.  Returning in image.\n");
+                    psFree(overscanVector);
+                    return(in);
+                }
+            } else if (fit == PM_FIT_SPLINE) {
+                // XXX: This makes no sense
+                // XXX: must free mySpline?
+                mySpline = (psSpline1D *) fitSpec;
+                mySpline = psVectorFitSpline1D(NULL, overscanVector);
+                if (mySpline == NULL) {
+                    psError(PS_ERR_UNKNOWN, false, "Could not fit a spline to overscan vector.  Returning in image.\n");
+                    psFree(overscanVector);
+                    return(in);
+                }
+            }
+
+            //
+            // Subtract fitted overscan vector row-wise from the image.
+            //
+            if (overScanAxis == PM_OVERSCAN_ROWS) {
+                for (i=0;i<(in->image)->numCols;i++) {
+                    psF32 tmpF32 = 0.0;
+                    if (fit == PM_FIT_POLYNOMIAL) {
+                        tmpF32 = psPolynomial1DEval(myPoly, ((psF32) i) / ((psF32) nBin));
+                    } else if (fit == PM_FIT_SPLINE) {
+                        tmpF32 = psSpline1DEval(mySpline, ((psF32) i) / ((psF32) nBin));
+                    }
+                    for (j=0;j<(in->image)->numRows;j++) {
+                        (in->image)->data.F32[j][i]-= tmpF32;
+                    }
+                }
+            }
+
+            //
+            // Subtract fitted overscan vector column-wise from the image.
+            //
+            if (overScanAxis == PM_OVERSCAN_COLUMNS) {
+                for (i=0;i<(in->image)->numRows;i++) {
+                    psF32 tmpF32 = 0.0;
+                    if (fit == PM_FIT_POLYNOMIAL) {
+                        tmpF32 = psPolynomial1DEval(myPoly, ((psF32) i) / ((psF32) nBin));
+                    } else if (fit == PM_FIT_SPLINE) {
+                        tmpF32 = psSpline1DEval(mySpline, ((psF32) i) / ((psF32) nBin));
+                    }
+
+                    for (j=0;j<(in->image)->numCols;j++) {
+                        (in->image)->data.F32[i][j]-= tmpF32;
+                    }
+                }
+            }
+        } else {
+            //
+            // If we get here, then no polynomials were fit to the overscan
+            // vector.  We simply subtract it, taking into account binning,
+            // from the image.
+            //
+
+            //
+            // Subtract overscan vector row-wise from the image.
+            //
+            if (overScanAxis == PM_OVERSCAN_ROWS) {
+                for (i=0;i<(in->image)->numCols;i++) {
+                    for (j=0;j<(in->image)->numRows;j++) {
+                        (in->image)->data.F32[j][i]-= overscanVector->data.F32[i/nBin];
+                    }
+                }
+            }
+
+            //
+            // Subtract overscan vector column-wise from the image.
+            //
+            if (overScanAxis == PM_OVERSCAN_COLUMNS) {
+                for (i=0;i<(in->image)->numRows;i++) {
+                    for (j=0;j<(in->image)->numCols;j++) {
+                        (in->image)->data.F32[i][j]-= overscanVector->data.F32[i/nBin];
+                    }
+                }
+            }
+        }
+
+        psFree(overscanVector);
+
+        tmpOverscan = tmpOverscan->next;
+    }
+
+    psTrace(".psModule.pmSubtracBias.pmSubtractBias", 4,
+            "---- pmSubtractBias() exit ----\n");
+
+    if (bias != NULL) {
+        return(SubtractFrame(in, bias));
+    }
+    return(in);
+}
+
+
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/pmSubtractBias.h	(revision 7462)
@@ -0,0 +1,50 @@
+/** @file  pmSubtractBias.h
+ *
+ *  This file will contain a module which will subtract the detector bias
+ *  in place from an input image.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2005-11-03 01:30:32 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#if !defined(PM_SUBTRACT_BIAS_H)
+#define PM_SUBTRACT_BIAS_H
+
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include<stdio.h>
+#include<math.h>
+#include "pslib.h"
+
+#include "pmFPA.h"   //#include "pmAstrometry.h"
+
+typedef enum {
+    PM_OVERSCAN_NONE,                         ///< No overscan subtraction
+    PM_OVERSCAN_ROWS,                         ///< Subtract rows
+    PM_OVERSCAN_COLUMNS,                      ///< Subtract columns
+    PM_OVERSCAN_ALL                           ///< Subtract the statistic of all pixels in overscan region
+} pmOverscanAxis;
+
+typedef enum {
+    PM_FIT_NONE,                              ///< No fit
+    PM_FIT_POLYNOMIAL,                        ///< Fit polynomial
+    PM_FIT_SPLINE                             ///< Fit cubic splines
+} pmFit;
+
+pmReadout *pmSubtractBias(pmReadout *in,                ///< The input pmReadout image
+                          void *fitSpec,                ///< A polynomial or spline, defining the fit type.
+                          const psList *overscans,      ///< A psList of overscan images
+                          pmOverscanAxis overScanAxis,  ///< Defines how overscans are applied
+                          psStats *stat,                ///< The statistic to be used in combining overscan data
+                          int nBin,                     ///< The amount of binning to be done image pixels.
+                          pmFit fit,                    ///< PM_FIT_SPLINE, PM_FIT_POLYNOMIAL, or PM_FIT_NONE
+                          const pmReadout *bias);       ///< A possibly NULL bias pmReadout which is to be subtracted
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.c
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.c	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.c	(revision 7462)
@@ -0,0 +1,567 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+#include "psAdditionals.h"
+
+
+psMetadata *pap_psMetadataCopy(psMetadata *out,
+			       const psMetadata *in)
+{
+    PS_ASSERT_PTR_NON_NULL(in,NULL);
+    if (out ==  NULL) {
+        out = psMetadataAlloc();
+    }
+    psMetadataItem *inItem = NULL;
+    psMetadataIterator *iter = psMetadataIteratorAlloc(*(psMetadata**)&in, PS_LIST_HEAD, NULL);
+    unsigned long numPointers = 0;	// Number of pointers we were forced to copy
+    while (inItem = psMetadataGetAndIncrement(iter)) {
+	psMetadataItem *outItem = NULL;
+
+	// Need to look for MULTI, which won't be picked up using the iterator.
+	psMetadataItem *multiCheckItem = psMetadataLookup(in, inItem->name);
+	unsigned int flag = PS_META_REPLACE; // Flag to indicate MULTI; otherwise, replace
+	if (multiCheckItem->type == PS_DATA_METADATA_MULTI) {
+	    psTrace(__func__, 10, "MULTI: %s (%s)\n", inItem->name, inItem->comment);
+	    flag = PS_DATA_METADATA_MULTI;
+	}
+
+	psTrace(__func__, 5, "Copying %s (%s)...\n", inItem->name, inItem->comment);
+
+#define PS_METADATA_COPY_CASE(NAME,TYPE) \
+          case PS_TYPE_##NAME: \
+            if (! psMetadataAdd(out, PS_LIST_TAIL, inItem->name, PS_TYPE_##NAME | flag, inItem->comment, \
+			        inItem->data.TYPE)) { \
+	        psErrorStackPrint(stderr, "Error copying %s (%s) in the metadata\n", inItem->name, \
+                                  inItem->comment); \
+            } \
+            break;
+
+	switch (inItem->type) {
+	    // Numerical types
+	    PS_METADATA_COPY_CASE(BOOL,B);
+	    PS_METADATA_COPY_CASE(S8,S8);
+	    PS_METADATA_COPY_CASE(S16,S16);
+	    PS_METADATA_COPY_CASE(S32,S32);
+	    PS_METADATA_COPY_CASE(U8,U8);
+	    PS_METADATA_COPY_CASE(U16,U16);
+	    PS_METADATA_COPY_CASE(U32,U32);
+	    PS_METADATA_COPY_CASE(F32,F32);
+	    PS_METADATA_COPY_CASE(F64,F64);
+
+	    // String: relying on the fact that this will copy the string, not point at it.
+	  case PS_DATA_STRING:
+            psMetadataAdd(out, PS_LIST_TAIL, inItem->name, PS_DATA_STRING | flag, inItem->comment,
+			  inItem->data.V);
+            break;
+
+	    // Metadata: copy the next level and stuff that in too
+	  case PS_DATA_METADATA:
+	    {
+		psMetadata *metadata = pap_psMetadataCopy(NULL, inItem->data.md);
+		psMetadataAdd(out, PS_LIST_TAIL, inItem->name, PS_DATA_METADATA | flag, inItem->comment,
+			      metadata);
+		break;
+	    }
+	    // Other kinds of pointers
+	  default:
+	    numPointers++;
+	    psTrace(__func__, 10, "Copying a pointer in the metadata: %x\n", inItem->type);
+	    psMetadataAdd(out, PS_LIST_TAIL, inItem->name, inItem->type | flag, inItem->comment,
+			  inItem->data.V);
+	    break;
+	}
+    }
+    psFree(iter);
+
+    if (numPointers > 0) {
+	psLogMsg(__func__, PS_LOG_WARN, "Forced to copy %d pointers when copying metadata.  Updating the "
+		 "copied psMetadata will affect the original!\n", numPointers);
+    }
+
+    return out;
+}
+
+
+psMetadata *psMetadataLookupMD(bool *status, const psMetadata *md, const char *key)
+{
+    psMetadataItem *item = psMetadataLookup((psMetadata*)md, key); // The metadata with instruments
+    psMetadata *value = NULL;		// The value to return
+    if (!item) {
+	// The given key isn't in the metadata
+	if (status) {
+	    *status = false;
+	} else {
+	    psError(PS_ERR_IO, true, "Couldn't find %s in the metadata.\n");
+	}
+    } else if (item->type != PS_DATA_METADATA) {
+	// The value at the key isn't metadata
+	if (status) {
+	    *status = false;
+	} else {
+	    psLogMsg(__func__, PS_LOG_WARN, "%s isn't of type PS_DATA_METADATA, as expected.\n");
+	}
+	value = NULL;
+    } else {
+	// We have the requested metadata
+	if (status) {
+	    *status = true;
+	}
+	value = item->data.md; // The requested metadata
+    }
+    return value;
+}
+
+
+char *psMetadataLookupString(bool *status, const psMetadata *md, const char *key)
+{
+    psMetadataItem *item = psMetadataLookup((psMetadata*)md, key); // The metadata with instruments
+    char *value = NULL;			// The value to return
+    if (!item) {
+	// The given key isn't in the metadata
+	if (status) {
+	    *status = false;
+	} else {
+	    psError(PS_ERR_IO, true, "Couldn't find %s in the metadata.\n");
+	}
+    } else if (item->type != PS_DATA_STRING) {
+	// The value at the key isn't of the desired type
+	if (status) {
+	    *status = false;
+	} else {
+	    psLogMsg(__func__, PS_LOG_WARN, "%s isn't of type PS_DATA_STRING, as expected.\n");
+	}
+	value = NULL;
+    } else {
+	// We have the requested metadata
+	if (status) {
+	    *status = true;
+	}
+	value = item->data.V; // The requested metadata
+    }
+    return value;
+}
+
+
+void psMetadataPrint(psMetadata *md, int level)
+{
+    psMetadataIterator *iter = psMetadataIteratorAlloc(md, PS_LIST_HEAD, NULL);	// Iterator
+    psMetadataItem *item = NULL;	// Item from metadata
+    while (item = psMetadataGetAndIncrement(iter)) {
+	// Indent...
+	for (int i = 0; i < level; i++) {
+	    printf(" ");
+	}
+	printf("%s", item->name);
+	if (item->comment && strlen(item->comment) > 0) {
+	    printf(" (%s)", item->comment);
+	}
+	printf(": ");
+	switch (item->type) {
+	  case PS_DATA_STRING:
+	    printf("%s", item->data.V);
+	    break;
+	  case PS_DATA_BOOL:
+	    if (item->data.B) {
+		printf("True");
+	    } else {
+		printf("False");
+	    }
+	    break;
+	  case PS_DATA_S32:
+	    printf("%d", item->data.S32);
+	    break;
+	  case PS_DATA_F32:
+	    printf("%f", item->data.F32);
+	    break;
+	  case PS_DATA_F64:
+	    printf("%f", item->data.F64);
+	    break;
+	  case PS_DATA_METADATA:
+	    printf("\n");
+	    psMetadataPrint(item->data.V, level + 1);
+	    break;
+	  default:
+	    printf("\n");
+	    psError(PS_ERR_IO, false, "Non-printable metadata type: %x\n", item->type);
+	}
+	printf("\n");
+    }
+    psFree(iter);
+
+    return;
+}
+
+
+// Set verbosity level
+int psArgumentVerbosity(int *argc, char **argv)
+{
+    int logLevel = 2;			// Default log level
+    int argnum = 0;			// Argument number
+
+    // set in order, so that -vvv overrides -vv overrides -v
+    if (argnum = psArgumentGet(*argc, argv, "-v")) {
+        psArgumentRemove(argnum, argc, argv);
+        logLevel = 3;
+    }
+    if (argnum = psArgumentGet(*argc, argv, "-vv")) {
+        psArgumentRemove(argnum, argc, argv);
+        logLevel = 4;
+    }
+    if (argnum = psArgumentGet(*argc, argv, "-vvv")) {
+        psArgumentRemove(argnum, argc, argv);
+        logLevel = 5;
+    }
+    psLogSetLevel (logLevel);		// XXX: This function should return an error if the log level is invalid
+
+    if (argnum = psArgumentGet(*argc, argv, "-logfmt")) {
+        if (*argc < argnum + 2) {
+            psError(PS_ERR_IO, true, "-logfmt switch specified without a format.");
+        } else {
+	    psArgumentRemove(argnum, argc, argv);
+	    psLogSetFormat(argv[argnum]); // XXX EAM : this function should return an error if the log format is invalid
+	    psArgumentRemove(argnum, argc, argv);
+	}
+    }
+
+    // Now the trace stuff
+    // argument format is: -trace (facil) (level)
+    while (argnum = psArgumentGet(*argc, argv, "-trace")) {
+        if (*argc < argnum + 3) {
+            psError(PS_ERR_IO, true, "-trace switch specified without facility and level.");
+        }
+        psArgumentRemove(argnum, argc, argv);
+        psTraceSetLevel(argv[argnum], atoi(argv[argnum+1])); // XXX: This function should return an error if the trace level is invalid
+        psArgumentRemove(argnum, argc, argv);
+        psArgumentRemove(argnum, argc, argv);
+    }
+    if ((argnum = psArgumentGet(*argc, argv, "-trace-levels"))) {
+        psTracePrintLevels();
+        exit(2);
+    }
+
+    return logLevel;
+}
+ 
+// Find the location of the specified argument
+int psArgumentGet(int argc, char **argv, const char *arg)
+{
+    for (int i = 1; i < argc; i++) {
+        if (!strcmp(argv[i], arg))
+            return i;
+    }
+   
+    return 0;
+}
+
+// Remove the specified argument (by location)
+bool psArgumentRemove(int argnum, int *argc, char **argv)
+{
+    if (argnum > 0) {
+        (*argc)--;
+        for (int i = argnum; i < *argc; i++) {
+            argv[i] = argv[i+1];
+        }
+    } else {
+	return false;
+    }
+    
+    return true;
+}
+
+
+static psMetadataItem *argumentRead(psMetadataItem *item, // Item to read into
+				    int argnum,	// Argument number
+				    int *argc,	// Number of arguments in total
+				    char **argv	// The arguments
+				    )
+{
+    psMetadataItem *newItem = NULL;
+    switch(item->type) {
+	// Only doing a representative set of types
+      case PS_DATA_S32:
+	newItem = psMetadataItemAlloc(item->name, item->type, item->comment, atoi(argv[argnum]));
+	psArgumentRemove(argnum, argc, argv);
+	break;
+      case PS_DATA_F32:
+	newItem = psMetadataItemAlloc(item->name, item->type, item->comment, atof(argv[argnum]));
+	psArgumentRemove(argnum, argc, argv);
+	break;
+      case PS_DATA_BOOL:
+	// Turn option on; no optional argument to remove
+	newItem = psMetadataItemAlloc(item->name, item->type, item->comment, true);
+	break;
+	// XXX: Include the other numerical types
+      case PS_DATA_STRING:
+	{
+	    //psString string = psStringCopy(argv[argnum]);	// Get the argument into PS memory management
+	    //psFree(string);
+	    newItem = psMetadataItemAlloc(item->name, item->type, item->comment, argv[argnum]);
+	    psArgumentRemove(argnum, argc, argv);
+	}
+	break;
+      default:
+	psError(PS_ERR_IO, true, "Argument type (%x) is not supported --- argument %s ignored\n",
+		item->type, item->name);
+	psFree(newItem);
+	return NULL;
+    }
+
+    return newItem;
+}
+
+
+// XXX: There is a memory leak in the MULTI section.  I think it might have something to do with reference
+// counting between lists and MD, in the second section of the code (copy newArgs into arguments), but I'm not
+// entirely sure.
+bool psArgumentParse(psMetadata *arguments, int *argc, char **argv)
+{
+    // We need to do a bit of mucking around in order to preserve the arguments metadata until the last
+    // minute --- if there is a bad argument, we need to return the old "arguments", since they contain
+    // the default values, which we probably want to output in a "help" message (we don't want to print
+    // the changed values and have the user think that they are default values).
+
+    psMetadata *newArgs = psMetadataAlloc(); // Place to read arguments into
+    psList *changed = psListAlloc(NULL);// List of keys that have changed
+
+    for (int i = 1; i < *argc; i++) {
+	psTrace(__func__, 7, "Looking at %s\n", argv[i]);
+	psMetadataItem *argItem = psMetadataLookup(arguments, argv[i]);
+	if (argItem) {
+	    psArgumentRemove(i, argc, argv); // Remove the switch
+	    if (argItem->type != PS_DATA_METADATA_MULTI) {
+		if (argItem->type != PS_DATA_BOOL && *argc < i + 1) {
+		    psError(PS_ERR_IO, true, "Required argument for %s is missing.\n", argItem->name);
+		    // XXX: Cleanup before returning
+		    psFree(newArgs);
+		    return false;
+		}
+		psMetadataItem *newItem = argumentRead(argItem, i, argc, argv);
+		psMetadataAddItem(newArgs, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+		psFree(newItem);
+	    } else {
+		// Go through the MULTI
+		psList *multi = argItem->data.V; // The list of MULTI psMetadataItems
+		if (*argc < i + multi->n) {
+		    psError(PS_ERR_IO, true, "Not enough arguments for %s.\n", argItem->name);
+		    // Remove the arguments --- they will be ignored
+		    for (int j = i; i < *argc; i++) {
+			psArgumentRemove(i, argc, argv);
+		    }
+		    // XXX: Cleanup before returning
+		    psFree(newArgs);
+		    return false;
+		}
+
+		// Remove any prior existence in the newArgs --- this is important because we specify
+		// adding the new items as DUPLICATE_OK, so if some idiot specifies it twice, we'd end
+		// up with two copies of everything.
+		psMetadataItem *checkItem = psMetadataLookup(newArgs, argItem->name);
+		if (checkItem) {
+		    (void)psMetadataRemove(newArgs, 0, argItem->name);
+		    (void)psListRemoveData(changed, argItem->name);
+		}
+
+		psListIterator *multiIter = psListIteratorAlloc(multi, PS_LIST_HEAD, true);
+		psMetadataItem *nextItem = NULL; // Item from list
+		while (nextItem = psListGetAndIncrement(multiIter)) {
+		    psMetadataItem *newItem = argumentRead(nextItem, i, argc, argv);
+		    psMetadataAddItem(newArgs, newItem, PS_LIST_TAIL, PS_META_DUPLICATE_OK);
+		    //psFree(newItem);
+		}
+		psFree(multiIter);
+	    }
+
+	    // Some book-keeping
+	    //	    psString name = psStringCopy(argItem->name);
+	    psListAdd(changed, PS_LIST_TAIL, argItem->name);
+	    i--;
+
+	} else if (strncmp(argv[i], "-", 1) == 0 || strncmp(argv[i], "+", 1) == 0) {
+	    // Someone's specified a bad option
+	    psError(PS_ERR_IO, true, "Unknown option: %s\n", argv[i]);
+	    psFree(newArgs);
+	    return false;
+	}
+    }
+
+    // All the arguments are good, so now we can copy the newArgs over
+    psListIterator *changedIter = psListIteratorAlloc(changed, PS_LIST_HEAD, false); // Iterator
+    psString name = NULL;		// Item from iteration
+    while (name = psListGetAndIncrement(changedIter)) {
+	printf("Updating %s\n", name);
+	psMetadataItem *oldItem = psMetadataLookup(arguments, name);
+	psMetadataItem *newItem = psMetadataLookup(newArgs, name);
+	if (oldItem->type != newItem->type) {
+	    psAbort(__func__, "Shouldn't reach here!\n");
+	}
+	switch (oldItem->type) {
+	    // Only doing a representative set of types
+	  case PS_DATA_S32:
+	    oldItem->data.S32 = newItem->data.S32;
+	    break;
+	  case PS_DATA_F32:
+	    oldItem->data.F32 = newItem->data.F32;
+	    break;
+	  case PS_DATA_BOOL:
+	    oldItem->data.B = newItem->data.B;
+	    break;
+	    // XXX: Include the other numerical types
+	  case PS_DATA_STRING:
+	    psFree(oldItem->data.V);
+	    oldItem->data.V = psMemIncrRefCounter(newItem->data.V);
+	    break;
+	  case PS_DATA_METADATA_MULTI:
+	    {
+		psList *newMulti = psMemIncrRefCounter(newItem->data.V);	// The new list of MULTI
+		psList *oldMulti = oldItem->data.V;	// The old list of MULTI
+		psListIterator *newMultiIter = psListIteratorAlloc(newMulti, PS_LIST_HEAD, false);
+		psListIterator *oldMultiIter = psListIteratorAlloc(oldMulti, PS_LIST_HEAD, true);
+		psMetadataItem *newMultiItem = NULL; // Item from iterator
+		while (newMultiItem = psListGetAndIncrement(newMultiIter)) {
+		    psMetadataItem *oldMultiItem = psListGetAndIncrement(oldMultiIter);
+		    if (! oldMultiItem) {
+			psAbort(__func__, 
+				"Something went very wrong here!  The lists SHOULD be of the same length!\n");
+		    }
+		    switch (oldMultiItem->type) {
+			// Only doing a representative set of types
+		      case PS_DATA_S32:
+			oldItem->data.S32 = newItem->data.S32;
+			break;
+		      case PS_DATA_F32:
+			oldItem->data.F32 = newItem->data.F32;
+			break;
+			// XXX: Include the other numerical types
+		      case PS_DATA_STRING:
+			psFree(oldItem->data.V);
+			oldItem->data.V = psMemIncrRefCounter(newItem->data.V);
+			break;
+		      default:
+			psAbort(__func__, "Should never ever get here, ever.\n");
+		    }
+		    psFree(oldMultiItem);
+		    psFree(newMultiItem);
+		}
+		psFree(newMultiIter);
+		psFree(oldMultiIter);
+	    }
+	    break;
+	  default:
+	    psAbort(__func__, "Should never ever ever get here.\n");
+	}
+    }
+    psFree(changedIter);
+    psFree(changed);
+
+    // Now, blow away the newArgs and we're done.
+    psFree(newArgs);
+    return true;
+}
+
+
+static int argLength(psMetadataItem *arg)
+{
+    switch (arg->type) {
+	// Only doing a representative set of types
+      case PS_DATA_S32:
+	return arg->data.S32 >= 0 ? (int)log10f((float)arg->data.S32) + 1 :
+	    (int)log10f(-(float)arg->data.S32) + 2;
+	// XXX: Other numerical types
+      case PS_DATA_F32:
+	return arg->data.F32 >= 0 ? 12 : 13; // -d.dddddde?dd
+      case PS_DATA_F64:
+	return arg->data.F64 >= 0 ? 12 : 13; // -d.dddddde?dd
+      case PS_DATA_BOOL:
+	return arg->data.B ? 4 : 5;
+      case PS_DATA_STRING:
+	return strlen(arg->data.V);
+      default:
+	psAbort(__func__, "Argument type (%x) is not supported.\n", arg->type);
+    }
+ 
+    return 0;
+}
+
+#define NUM_SPACES 4			// Number of spaces between
+
+void psArgumentHelp(psMetadata *arguments)
+{
+    printf("Optional arguments, with default values:\n");
+    psMetadataIterator *argIter = psMetadataIteratorAlloc(arguments, PS_LIST_HEAD, NULL);
+    psMetadataItem *argItem = NULL;	// Item from iterator
+    int maxName = 4;			// Maximum length of a name
+    int maxValue = 4;			// Maximum length of a value
+
+    // First pass to get the sizes
+    while (argItem = psMetadataGetAndIncrement(argIter)) {
+	if (strlen(argItem->name) > maxName) {
+	    maxName = strlen(argItem->name);
+	}
+	int valLength = argLength(argItem);
+	if (valLength > maxValue) {
+	    maxValue = valLength;
+	}
+    }
+
+    // Second pass to print
+    psMetadataIteratorSet(argIter, PS_LIST_HEAD);
+    psString lastName = NULL;		// Last name we printed
+    while (argItem = psMetadataGetAndIncrement(argIter)) {
+	// Initial indent
+	for (int i = 0; i < NUM_SPACES; i++) {
+	    printf(" ");
+	}
+
+	// Print the name if required
+	int position = 0;	// Number of spaces in
+	if (! lastName || strcmp(lastName, argItem->name) != 0) {
+	    // A new name
+	    printf("%s", argItem->name);
+	    position += strlen(argItem->name);
+	    lastName = argItem->name;
+	}
+	for (int i = position; i < maxName + NUM_SPACES; i++) {
+	    printf(" ");
+	}
+
+	// Print the value
+	printf("(");
+	switch (argItem->type) {
+	    // Only doing a representative set of types
+	  case PS_DATA_S32:
+	    printf("%d", argItem->data.S32);
+	    break;
+	    // XXX: Other numerical types
+	  case PS_DATA_F32:
+	    printf("%.6e", argItem->data.F32);
+	    break;
+	  case PS_DATA_F64:
+	    printf("%.6e", argItem->data.F64);
+	    break;
+	  case PS_DATA_BOOL:
+	    if (argItem->data.B) {
+		printf("TRUE");
+	    } else {
+		printf("FALSE");
+	    }
+	    break;
+	  case PS_DATA_STRING:
+	    printf("%s", argItem->data.V);
+	    break;
+	  default:
+	    psAbort(__func__, "Argument type (%x) is not supported.\n", argItem->type);
+	}
+	printf(")");
+	for (int i = argLength(argItem); i < maxValue + NUM_SPACES; i++) {
+	    printf(" ");
+	}
+
+	// Print the comment
+	if (argItem->comment) {
+	    printf("%s", argItem->comment);
+	}
+	printf("\n");
+    }
+
+    psFree(argIter);
+}
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.h
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.h	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/psAdditionals.h	(revision 7462)
@@ -0,0 +1,30 @@
+#ifndef PS_ADDITIONALS_H
+#define PS_ADDITIONALS_H
+
+#include "pslib.h"
+
+// Deep copy of metadata
+psMetadata *pap_psMetadataCopy(psMetadata *out, const psMetadata *in);
+
+// Get a value from the metadata that we believe should be metadata.
+psMetadata *psMetadataLookupMD(bool *status, const psMetadata *md, const char *key);
+
+// Get a value from the metadata that we believe should be a string
+char *psMetadataLookupString(bool *status, const psMetadata *md, const char *key);
+
+#if 0
+pmChip *psMetadataLookupChip(bool *status, const psMetadata *md, const char *key);
+pmCell *psMetadataLookupCell(bool *status, const psMetadata *md, const char *key);
+#endif
+
+// Print out the metadata
+void psMetadataPrint(psMetadata *md, int level);
+
+// Argument handling
+int psArgumentVerbosity(int *argc, char **argv);
+int psArgumentGet(int argc, char **argv, const char *arg);
+bool psArgumentRemove(int argnum, int *argc, char **argv);
+bool psArgumentParse(psMetadata *arguments, int *argc, char **argv);
+void psArgumentHelp(psMetadata *arguments);
+
+#endif
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/readouts.txt
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/readouts.txt	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/readouts.txt	(revision 7462)
@@ -0,0 +1,41 @@
+Behaviour of readouts:
+
+
+typedef struct {
+	const char *extname;
+	psMetadata *header;
+	psArray *images;
+	psArray *masks;
+	psArray *weights;
+} pmPixelData;
+
+typedef struct {
+	...
+	psMetadata *concepts;
+	pmCell *parent;
+	psImage *image;
+	psImage *mask;
+	psImage *weight;
+} pmReadout;
+
+
+Procedures:
+
+a. Read: set pmReadout->image = pmPixelData->images->data[i],
+   pmReadout->mask = pmPixelData->masks->data[i], pmReadout->weight =
+   pmPixelData->weights->data[i]
+
+b. Mask bad pixels: Copy the provided mask into the input image mask.
+
+c. Bias/overscan subtraction: Read the CELL.BIASSEC, and apply to the
+   CELL.TRIMSEC.
+
+d. Trim: Cut the CELL.TRIMSEC, and paste into pmReadout->image,
+   pmReadout->mask, pmReadout->weight.
+
+e. Flat-fielding: Pretty straight-forward.  The FF just needs to be of
+   the same format as the input image.
+
+f. Splice: If images are untrimmed, then have to pluck out the
+   CELL.TRIMSEC and CELL.BIASSEC, and need to worry about the overscan
+   regions.
Index: /tags/rel-0_0_1/archive/scripts/src/phase2/testing.txt
===================================================================
--- /tags/rel-0_0_1/archive/scripts/src/phase2/testing.txt	(revision 7462)
+++ /tags/rel-0_0_1/archive/scripts/src/phase2/testing.txt	(revision 7462)
@@ -0,0 +1,65 @@
+Overscans:
+
+Modes: NONE | INDIVIDUAL | ALL
+
+* NONE produces warnings that there is no overscan subtraction:
+  "Proceeding to full fram subtraction."  This is probably
+  unnecessary.  The second warning is "bias frame is NULL.  Returning
+  original image."  This is in the spec --- good.
+
+* INDIVIDUAL works well.  The exponential ramp at the bottom of the
+  megacam image is removed. It leaves horizontal streaks, but I've
+  seen these before, and I figure they're part of the instrument, not
+  our software.
+
+* ALL also works.  The bias is removed, but the ramp remains, which is
+  exactly what I expected.
+
+Bin: An integer.  Using INDIVIDUAL mode.
+
+* 1 removes the ramp
+* 0 removes the ramp
+* Large values (50,100) have the effect of smoothing out the
+  horizontal streaks.
+
+Stat: MEAN | MEDIAN
+
+* Do a similar job (not identical).  Slightly less horizontal
+  structure with median.
+
+Fit: NONE | POLYNOMIAL | SPLINE
+
+* NONE is fine.
+
+* POLYNOMIAL produces a SEGV when I try to print out the resultant
+  polynomial.  I realise I haven't told it the polynomial order
+  (should create a psPolynomial1D to feed in to specify the order),
+  but I would have expected an error message.  Fixing this up, the
+  polynomial fitting works fairly well.
+
+* SPLINE doesn't seem to work --- it won't give back a spline for me
+  to print out the results of the fit.  Filed a bug.
+
+
+Flat: it works, but I'm concerned it's doing the entire image area,
+instead of just the appropriate area (CELL.TRIMSEC).  I added a trim
+to the phase 2, and it runs faster now.  The program still outputs the
+overscan, beacause it writes the pixels it read it.  The trim only
+affects where the computations are done.  Flat-fielding the image by
+itself (w/o overscan) results in ones everywhere, except the overscan.
+ 
+Full-frame bias subtraction: Works, but it acts on the entire image.
+Fixed the definition so that this is only done on the region specified
+by CELL.TRIMSEC.  That way, the overscan of the input image isn't
+affected by the full-frame subtraction.
+
+Non-linearity: This is really slow, or there's something really weird
+going on.  It used a heap of memory.  OK, found the problem: I had an
+extraneous free that caused corruption of the number of elements in
+the vector of coefficients.  Again, it wants to act on the entire
+image, so adding to the spec that it should only act on the
+CELL.TRIMSEC (assuming the non-linearity is from the CCD itself, not
+the amplifier).  It's fairly slow, but that might be because of the
+multiple trace messages in the polynomial evaluation.  Got the
+individual correction parameters working.  Now need to do the lookup
+table.
Index: /tags/rel-0_0_1/ippTools/Makefile.am
===================================================================
--- /tags/rel-0_0_1/ippTools/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/Makefile.am	(revision 7462)
@@ -0,0 +1,3 @@
+SUBDIRS = src
+
+CLEANFILES = *~ core core.*
Index: /tags/rel-0_0_1/ippTools/autogen.sh
===================================================================
--- /tags/rel-0_0_1/ippTools/autogen.sh	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/autogen.sh	(revision 7462)
@@ -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=p2tools
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL=aclocal
+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: /tags/rel-0_0_1/ippTools/configure.ac
===================================================================
--- /tags/rel-0_0_1/ippTools/configure.ac	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/configure.ac	(revision 7462)
@@ -0,0 +1,27 @@
+AC_PREREQ(2.59)
+
+AC_INIT([p2tools], [0.0.1], [eugene@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+dnl AM_CONFIG_HEADER([config.h])
+AM_MAINTAINER_MODE
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC
+AC_PROG_INSTALL
+dnl AC_PROG_LIBTOOL
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 0.9.0]) 
+PKG_CHECK_MODULES([PSMODULES], [psmodule >= 0.9.1]) 
+PKG_CHECK_MODULES([METADATADB], [metadatadb >= 0.0.1]) 
+
+p2tools_CFLAGS="-Wall -Werror -std=c99"
+AC_SUBST([p2tools_CFLAGS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+])
+AC_OUTPUT
Index: /tags/rel-0_0_1/ippTools/doc/p2tools.sh
===================================================================
--- /tags/rel-0_0_1/ippTools/doc/p2tools.sh	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/doc/p2tools.sh	(revision 7462)
@@ -0,0 +1,97 @@
+
+## PanTasks scripts for Phase 2
+
+# tasks: p2pending p2update ppImage
+#
+# queues: 
+#   p2pending: (url) (expID) (cameraID) (chipID) (state)
+#
+# globals: 
+
+# p2pending : search for images to be processed
+task	       p2pending
+  command      p2search -pending
+  host         local
+
+  # timeout shorter than exec so jobs do not build up
+  periods      -exec     5
+  periods      -poll     1
+  periods      -timeout  4
+
+  # success
+  task.exit    0
+    local i Nstdout
+    # keep only new, unique entries (URL is key)
+    queuesize stdout -var Nstdout
+    for i 0 $Nstdout
+      queuepop stdout -var line
+      queuepush p2pending -uniq -key 0 "$line new"
+    end
+  end
+end
+
+# ppImage : submit an image for processing by ppImage
+task	       ppImage
+  periods      -exec 0.1
+  periods      -timeout 120
+
+  task.exec
+    local Npending
+    queuesize  p2pending -var Npending
+    if ($Npending == 0) break
+
+    # add in system status interruptions later
+    # if ($network == 0) break
+    
+    queuepop p2pending -var line -key 4 new
+    if ("$line" == "NULL") break
+
+    list tmp -split $line
+    $url    = $tmp:0
+    $expID  = $tmp:1
+    $camera = $tmp:2
+    $chip   = $tmp:3
+    queuepush p2pending "$url $expID $camera $chip run"
+
+    # need to decide real name for this function
+    $host = `chip.host $camera $chip`
+    host $host
+    command ppImage $url $chip
+  end
+
+  # success
+  task.exit    0
+    # if ppImage updates the DB:
+    queueinit stdout
+    queuepush p2pending $taskargs:0 $taskargs:1 $taskargs:2 $taskargs:3 done
+    
+    # if ppImage does NOT update the DB:
+    queueinit stdout
+    exec p2search -done $taskargs:0 $taskargs:1 $taskargs:2 
+    queuepush p2pending $taskargs:0 $taskargs:1 $taskargs:2 $taskargs:3 done
+  end
+
+end
+
+# p2update : search for images to be processed
+task	       p2update
+  command      p2search -update
+  host         local
+
+  # timeout shorter than exec so jobs do not build up
+  periods      -exec     5
+  periods      -poll     1
+  periods      -timeout  4
+end
+
+# p2done : search for images to be processed
+task	       p2done
+  command      p2search -done
+  host         local
+
+  # timeout shorter than exec so jobs do not build up
+  periods      -exec     5
+  periods      -poll     1
+  periods      -timeout  4
+end
+
Index: /tags/rel-0_0_1/ippTools/doc/p2tools.txt
===================================================================
--- /tags/rel-0_0_1/ippTools/doc/p2tools.txt	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/doc/p2tools.txt	(revision 7462)
@@ -0,0 +1,41 @@
+
+Phase 2 pipeline tools:
+
+p2search -quick 
+  * search for images which match in raw.exp,raw.imfiles
+  * output in format which can be used by ppImage pantasks script
+
+p2search -define [options]
+  * input: searches mddb:raw_exposures,raw_images
+        - compares against p2Pending[Exp|Imfile]
+        - compares against p2Done[Exp|Imfile]
+        - XXX should be implemented as a special wrapper function that
+          implements this comparison as a SQL query
+  * output: updates mddb:P2_exposures_pending,P2_images_pending
+  * alternative output: identical to p2pending
+ 
+p2search -pending 
+  * input: searches mddb:P2_exposures_pending,P2_images_pending
+  * output: Nlines consisting of:
+    (URL) (expID) (class)
+  * options: ?
+
+p2search -update
+  * examine the imfiles and identify any completed exposures
+
+p2search -done
+  * get exp_id/class/class_id from the CLI
+  * add the completed imfile to the p2DoneImfile tables
+  * remove corresponding entries from the p2PendingImfile table
+  * check to see if any p2PendingExps have no associated p2PendingImfiles
+        if so move the p2PendingExp(s) to p2DoneExp
+//  * send new entry to the pending p3 table
+
+ppImage file://path/filename file://path/outroot -recipe (recipe) 
+ppImage neb://nebname neb://outroot -recipe (recipe)
+
+restriction options:
+  -time (start) (stop)
+  -camera (camera) 
+  -region (ra,dec) (ra,dec)
+
Index: /tags/rel-0_0_1/ippTools/src/Makefile.am
===================================================================
--- /tags/rel-0_0_1/ippTools/src/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/Makefile.am	(revision 7462)
@@ -0,0 +1,40 @@
+bin_PROGRAMS = p2search p2admin
+
+p2search_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(METADATADB_CFLAGS) $(p2tools_CFLAGS)
+p2search_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(METADATADB_LIBS)
+p2search_SOURCES = \
+    p2alloc.c \
+    p2insertDoneFrames.c \
+    p2insertPendingFrames.c \
+    p2pendingToDone.c \
+    p2rawToPending.c \
+    p2search.c \
+    p2searchConfig.c \
+	p2searchDoneFrames.c \
+    p2searchPendingFrames.c \
+    p2searchRawFrames.c \
+    p2updatePending.c \
+    p2writePendingFrames.c
+
+p2admin_CPPFLAGS = $(PSLIB_CFLAGS) $(PSMODULES_CFLAGS) $(METADATADB_CFLAGS) $(p2tools_CFLAGS)
+p2admin_LDADD = $(PSLIB_LIBS) $(PSMODULES_LIBS) $(METADATADB_LIBS)
+p2admin_SOURCES = \
+    p2alloc.c \
+    p2admin.c \
+    p2adminConfig.c \
+    p2adminTables.c \
+    p2insertDoneFrames.c \
+    p2insertPendingFrames.c \
+    p2pendingToDone.c \
+    p2rawToPending.c \
+    p2updatePending.c \
+    p2writePendingFrames.c
+
+noinst_HEADERS = \
+	p2tools.h
+
+clean-local:
+	-rm -f TAGS
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
Index: /tags/rel-0_0_1/ippTools/src/chiptool.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/chiptool.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/chiptool.c	(revision 7462)
@@ -0,0 +1,238 @@
+#include <stdlib.h>
+
+#include "p2tools.h"
+
+static bool quickMode(p2Config *config);
+static bool defineMode(p2Config *config);
+static bool pendingMode(p2Config *config);
+static bool doneMode(p2Config *config);
+
+int main(int argc, char **argv) {
+    p2Config        config; 
+
+    p2searchConfig(&config, argc, argv);
+
+    switch (config.mode) {
+        case P2_MODE_QUICK:
+            if (!quickMode(&config)) {
+                exit(EXIT_FAILURE);
+            }
+            break;
+        case P2_MODE_DEFINE:
+            if (!defineMode(&config)) {
+                exit(EXIT_FAILURE);
+            }
+            break;
+        case P2_MODE_PENDING:
+            if (!pendingMode(&config)) {
+                exit(EXIT_FAILURE);
+            }
+            break;
+    /*
+    if (config.mode == P2_MODE_UPDATE) {
+        bool status = p2updatePendingFrames(&config, pendingFrames);
+        if (!status) {
+            psAbort(argv[0], "p2updatePendingFrames() failed");
+        }
+    }
+    */
+        case P2_MODE_DONE:
+            if (!doneMode(&config)) {
+                exit(EXIT_FAILURE);
+            }
+            break;
+        default:
+            psAbort(argv[0], "invalid option (this should not happen)");
+    }
+
+    exit(EXIT_SUCCESS);
+}
+
+static bool quickMode(p2Config *config)
+{
+    psArray *rawFrames = p2searchRawFrames(config);
+    if (!rawFrames) {
+        psError(PS_ERR_UNKNOWN, false, "no ppRawFrames found");
+        return false;
+    }
+    psArray *pendingFrames = p2rawToPending(config, rawFrames);
+    if (!pendingFrames) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingFrames found");
+        return false;
+    }
+    bool status = p2writePendingFrames(config, pendingFrames);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "p2writePendingFrames() failed");
+        return false;
+    }
+
+    return true;
+}
+
+static bool defineMode(p2Config *config)
+{
+    psArray *rawFrames = p2searchRawFrames(config);
+    if (!rawFrames) {
+        psError(PS_ERR_UNKNOWN, false, "no ppRawFrames found");
+        return false;
+    }
+    psArray *pendingFrames = p2searchPendingFrames(config);
+    // XXX compare raw frames to pending frames and remove duplicate 
+    // frames from the raw set.  This may not be quiet right as it's
+    // possible (likely?) that a rawScienceExp is inserted into the
+    // database prior to /ALL/ of that exposure's Imfiles
+    if (pendingFrames) {
+        for (int i = 0; i < rawFrames->n; i++) {
+            ppRawFrame *rawFrame = rawFrames->data[i];
+            for (int j = 0; j < pendingFrames->n; j++) {
+                p2PendingFrame *pendingFrame = pendingFrames->data[j];
+                if (strcmp(rawFrame->exposure->exp_id,
+                           pendingFrame->exposure->exp_id) == 0) {
+                    psArrayRemove(rawFrames, rawFrame);
+                    // dec the counter as the array just got shorter
+                    // and we don't want to skip elemnts
+                    i--;
+                    break;
+                } 
+            }
+        }
+
+        psFree(pendingFrames);
+    }
+
+    psArray *doneFrames = p2searchDoneFrames(config);
+    if (doneFrames && (rawFrames->n > 0)) {
+        for (int i = 0; i < rawFrames->n; i++) {
+            ppRawFrame *rawFrame = rawFrames->data[i];
+            for (int j = 0; j < pendingFrames->n; j++) {
+                p2DoneFrame *doneFrame = pendingFrames->data[j];
+                if (strcmp(rawFrame->exposure->exp_id,
+                           doneFrame->exposure->exp_id) == 0) {
+                    psArrayRemove(rawFrames, rawFrame);
+                    // dec the counter as the array just got shorter
+                    // and we don't want to skip elemnts
+                    i--;
+                    break;
+                } 
+            }
+        }
+
+        psFree(doneFrames);
+    }
+
+    if (!rawFrames->n > 0) {
+        psError(PS_ERR_UNKNOWN, false, "no unprocessed ppRawFrames found");
+        psFree(rawFrames);
+        return false;
+    }
+
+    bool status = p2insertPendingFrames(config, rawFrames);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false,"p2insertPendingFrames() failed");
+        return false;
+    }
+
+    return true;
+}
+
+static bool pendingMode(p2Config *config)
+{
+    psArray *pendingFrames = p2searchPendingFrames(config);
+    if (!pendingFrames) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingFrames found");
+        return false;
+    }
+    bool status = p2writePendingFrames(config, pendingFrames);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false,"p2insertPendingFrames() failed");
+        return false;
+    }
+
+    return true;
+}
+
+static bool doneMode(p2Config *config)
+{
+    // get exp_id/class/class_id/url from the CLI // add the completed imfile to
+    // the p2DoneImfile tables // remove corresponding entries from the
+    // p2PendingImfile table // check to see if any p2PendingExps have no
+    // associated p2PendingImfiles //    if so move the p2PendingExp(s) to
+    // p2DoneExp
+
+    // find pending
+    psArray *pendingImfiles = p2searchPendingImfiles(config);
+    if (!pendingImfiles) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingImfiles found");
+        return false;
+    }
+    // insert into done
+    psArray *doneImfiles = p2pendingToDoneImfile(pendingImfiles);
+    for (int i = 0; i < doneImfiles->n; i++) {
+        if (!p2DoneImfileInsertObject(config->database, doneImfiles->data[i])) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+            return false;
+        }
+    }
+    // delete from pending
+    if (p2PendingImfileDeleteRowObjects(config->database, pendingImfiles, MAX_ROWS) < 0) {
+        // there must be atleast 1 Imfile to get this far
+        psError(PS_ERR_UNKNOWN, false, "database access failed");
+        return false;
+    }
+
+    // look for pending exposures
+    psArray *pendingExps = p2searchPendingExp(config);
+    if (!pendingExps) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingExps found");
+        return false;
+    }
+
+    for (int i = 0; i < pendingExps->n; i++) {
+        p2PendingExpRow *pendingExp = pendingExps->data[i];
+
+        psMetadata *where = psMetadataAlloc();
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", 0, "==", pendingExp->exp_id);
+        psArray *pendingImfiles = p2PendingImfileSelectRowObjects(config->database, where, MAX_ROWS);
+        psFree(where)
+        if (!pendingImfiles) {
+            // exp has no coresponding imfiles
+            psArray *nukeMe = psArrayAlloc(1);
+            nukeMe->n = 0;
+            psArrayAdd(nukeMe, 0, pendingExp);
+            bool status = p2PendingExpDeleteRowObjects(config->database, nukeMe, MAX_ROWS);
+            psFree(nukeMe);
+            if (!status) {
+                psError(PS_ERR_UNKNOWN, false, "database access failed");
+                return false;
+            }
+            // XXX need a func to convert from p2PendingExp -> p2DoneExp
+
+            p2DoneExpRow *doneExp = p2pendingToDoneExp(pendingExp);
+            if (!doneExp) {
+                psError(PS_ERR_UNKNOWN, false, "p2PendingExp -> p2DoneExp failed");
+                return false;
+            }
+            if (!p2DoneExpInsertObject(config->database, doneExp)) {
+                psError(PS_ERR_UNKNOWN, false, "database access failed");
+                return false;
+            }
+        }
+        // skip phase 3 for the time being
+        psFree(pendingImfiles);
+    }
+
+    /*
+    psArray *doneFrames = p2pendingToDone(config, pendingFrames);
+    if (!doneFrames) {
+        psError(PS_ERR_UNKNOWN, false, "p2pendingToDone() failed");
+        return false;
+    }
+    bool status = p2insertDoneFrames(config, doneFrames);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, false, "p2insertDoneFrames() failed");
+        return false;
+    }
+    */
+
+    return true;
+}
Index: /tags/rel-0_0_1/ippTools/src/chiptoolConfig.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/chiptoolConfig.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/chiptoolConfig.c	(revision 7462)
@@ -0,0 +1,125 @@
+#include <pmConfig.h>
+
+#include "p2tools.h"
+
+bool p2searchConfig(p2Config *config, int argc, char **argv) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // Parse the configurations (re-org a la ppImage)
+    config->site    = NULL;             // Site configuration
+    config->camera  = NULL;             // Camera configuration (unused)
+    config->recipe  = NULL;             // Recipe configuration
+    config->camera_name = NULL;         // Camera name
+    config->filter  = NULL;             // Filter name
+    config->exp_id  = NULL;             // Exposure ID
+    config->class   = NULL;             
+    config->class_id = NULL;           
+    config->url     = NULL;
+
+    if (! pmConfigRead(&config->site, &config->camera, &config->recipe, &argc, argv, RECIPE)) {
+        psErrorStackPrint(stderr, "Can't find site configuration!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // Parse other command-line arguments
+    config->arguments = psMetadataAlloc(); // The arguments, with default values
+
+    int N;
+    config->mode = P2_MODE_NONE;
+    if ((N = psArgumentGet (argc, argv, "-quick"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (config->mode) {
+            psAbort (argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_QUICK;
+    }
+    if ((N = psArgumentGet (argc, argv, "-define"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (config->mode) {
+            psAbort (argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_DEFINE;
+    }
+    if ((N = psArgumentGet (argc, argv, "-pending"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (config->mode) {
+            psAbort (argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_PENDING;
+    }
+    /*
+    if ((N = psArgumentGet (argc, argv, "-update"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (config->mode) {
+            psAbort (argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_UPDATE;
+    }
+    */
+    if ((N = psArgumentGet (argc, argv, "-done"))) {
+        psArgumentRemove (N, &argc, argv);
+        if (config->mode) {
+            psAbort (argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_DONE;
+    }
+    
+    // paul's argument parsing convention requires: -key value 
+    psMetadataAddBool(config->arguments, PS_LIST_TAIL, "-quick",   0, "examine raw image table, write ppImage output", false);
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-define",  0, "examine raw image table, add to pending image table", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-pending", 0, "examine pending image table, write ppImage output", "");
+//    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-update",  0, "update pending image table", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-done",  0, "mov image from pending to done table", "");
+//    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-time",    0, "define time range of interest", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-inst",  0, "define camera of interest", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-filter",  0, "define filter of interest", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-exp_id",  0, "define exposure ID", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-class",  0, "define class", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-class_id",  0, "define class ID", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-url",  0, "define URL", "");
+
+    if (config->mode == P2_MODE_NONE) {
+        fprintf (stderr, "search mode not specified\n");
+        psArgumentHelp(config->arguments);
+        psFree(config->arguments);
+        exit(EXIT_FAILURE);
+    }
+
+    if (! psArgumentParse(config->arguments, &argc, argv) || argc != 1) {
+        printf("\nPan-STARRS Phase 2 Search Tool\n\n");
+        printf("Usage: %s [mode] [options]\n\n", argv[0]);
+        printf(" [mode] : -quick | -define | -pending | -donee\n\n");
+        psArgumentHelp(config->arguments);
+        psFree(config->arguments);
+        exit(EXIT_FAILURE);
+    }
+
+    bool status;
+    config->camera_name = psMetadataLookupStr(&status, config->arguments,
+        "-inst");
+    config->exp_id = psMetadataLookupStr(&status, config->arguments, "-exp_id");
+    config->class = psMetadataLookupStr(&status, config->arguments, "-class");
+    config->class_id = psMetadataLookupStr(&status, config->arguments,
+        "-class_id");
+    config->url = psMetadataLookupStr(&status, config->arguments, "-url");
+
+    // XXX why is "" being returned when -[foo] isn't specified?
+#define EMPTY_TO_NULL_STRING(var) \
+    if (strcmp(var, "") == 0) { \
+        var = NULL; \
+    }
+
+    EMPTY_TO_NULL_STRING(config->camera_name);
+    EMPTY_TO_NULL_STRING(config->exp_id);
+    EMPTY_TO_NULL_STRING(config->class);
+    EMPTY_TO_NULL_STRING(config->class_id);
+    EMPTY_TO_NULL_STRING(config->url);
+
+    // add the input and output images to the arguments list
+    //psMetadataAddStr (config->arguments, PS_LIST_TAIL, "-output", 0, "Name of the output image", argv[1]);
+
+    // define Database handle, if used
+    config->database = pmConfigDB(config->site);
+
+    return true;
+} 
Index: /tags/rel-0_0_1/ippTools/src/p2insertDoneFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2insertDoneFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2insertDoneFrames.c	(revision 7462)
@@ -0,0 +1,31 @@
+# include <metadatadb.h>
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+bool p2insertDoneFrames(p2Config *config, psArray *doneFrames) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(doneFrames, false);
+
+    for (int i = 0; i < doneFrames->n; i++) {
+        p2DoneFrame *doneFrame = doneFrames->data[i];
+        p2DoneExpRow *doneExposure = doneFrame->exposure;
+        
+        psArray *doneImages = doneFrame->images;
+        
+        if (!p2DoneExpInsertObject(config->database, doneExposure)) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+            return false;
+        }
+
+        for (int i = 0; i < doneImages->n; i++) {
+            if (!p2DoneImfileInsertObject(config->database, doneImages->data[i])) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+            return false;
+            }
+        }
+    }
+    return true;
+} 
+
+// XXX : must remove the done frames from the pending table, or we'll get duplicates
+// XXX : add P3pending insert function? selected on recipe type? 
Index: /tags/rel-0_0_1/ippTools/src/p2insertPendingFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2insertPendingFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2insertPendingFrames.c	(revision 7462)
@@ -0,0 +1,89 @@
+#include <metadatadb.h>
+
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+// NOTE this function converts the rawFrames to pendingFrames AND inserts them
+//      this is useful since the table must be locked the whole time, to get the versions
+bool p2insertPendingFrames(p2Config *config, psArray *rawFrames) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(rawFrames, false);
+
+    // can we use p2rawToPending() here?
+    for (int i = 0; i < rawFrames->n; i++) {
+        ppRawFrame *rawFrame = rawFrames->data[i];
+
+        // find the next available version numbers
+        // broken
+        /*
+        psMetadata *where = psMetadataAlloc();
+
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", PS_META_REPLACE, "==",
+            rawFrame->exposure->exp_id);
+        psArray *exposures = p2PendingExpSelectRowObjects(config->database,
+            where, MAX_ROWS);
+        psFree(where);
+        if (!exposures) {
+            psError(PS_ERR_UNKNOWN, false, "no p2PedningExps found");
+            return false;
+        }
+
+        int version = -1;
+        for (int j = 0; j < exposures->n; j++) {
+            p2PendingExpRow *exposure = exposures->data[j];
+            version = PS_MAX(version, exposure->p2_version);
+        }
+        version ++;
+        */
+
+        p2PendingExpRow *pendingExposure = p2PendingExpRowAlloc(
+            rawFrame->exposure->exp_id,
+            rawFrame->exposure->camera,
+            rawFrame->exposure->filter,
+            rawFrame->exposure->class,
+            rawFrame->exposure->nclass,
+            0xff,
+            0xff
+            // pendingExposure->Ndone = 0;
+        );
+
+        psArray *pendingImages = psArrayAlloc(rawFrame->images->n);
+        pendingImages->n = 0;
+        for (int j = 0; j < rawFrame->images->n; j++) {
+            rawImfileRow *rawImage = rawFrame->images->data[j];
+            p2PendingImfileRow *pendingImage = p2PendingImfileRowAlloc(
+                rawImage->exp_id,
+                rawImage->exptype,
+                rawImage->class,
+                rawImage->class_id,
+                0xff, // p1
+                0xff, // p2
+                "",
+                "",
+                // XXX cleanup the url somehow
+                rawImage->url
+            );
+
+            psArrayAdd(pendingImages, 100, pendingImage);
+        }
+        
+        if (!p2PendingExpInsertObject(config->database, pendingExposure)) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+            return false;
+        }
+
+        for (int i = 0; i < pendingImages->n; i++) {
+            if (!p2PendingImfileInsertObject(config->database,
+                    pendingImages->data[i])) {
+                psError(PS_ERR_UNKNOWN, false, "database access failed");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+// XXX the filename layout is defined by this code
+// XXX have the SQL command update the version number?
+// XXX EAM : need to lock the table : inner or outer loop?
Index: /tags/rel-0_0_1/ippTools/src/p2pendingToDone.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2pendingToDone.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2pendingToDone.c	(revision 7462)
@@ -0,0 +1,107 @@
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+psArray *p2pendingToDone(p2Config *config, psArray *pendingFrames) {
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(pendingFrames, NULL);
+
+    psArray *doneFrames = psArrayAlloc(pendingFrames->n);
+    doneFrames->n = 0;
+
+    for (int i = 0; i < pendingFrames->n; i++) {
+        p2PendingFrame *pendingFrame = pendingFrames->data[i];
+// if (pendingFrame->exposure->state != P2_STATE_DONE) continue;
+
+        p2DoneExpRow *doneExposure = p2DoneExpRowAlloc(
+            pendingFrame->exposure->exp_id,
+            pendingFrame->exposure->camera,
+            pendingFrame->exposure->filter,
+            pendingFrame->exposure->class,
+            pendingFrame->exposure->nclass, // XXX ndone is uneeded?
+            0xFF, // ndone
+            pendingFrame->exposure->p1_version,
+            pendingFrame->exposure->p2_version
+            //doneExposure->state = P2_STATE_DONE;
+        );
+
+        psArray *doneImages = psArrayAlloc(pendingFrame->images->n);
+        doneImages->n = 0;
+        for (int j = 0; j < pendingFrame->images->n; j++) {
+            p2PendingImfileRow *pendingImage = pendingFrame->images->data[j];
+
+            /*
+            if (pendingImage->state != P2_STATE_DONE) {
+                psAbort ("p2search", "programming error!");
+            }
+            */
+
+            p2DoneImfileRow *doneImage = p2DoneImfileRowAlloc(
+                pendingImage->exp_id,
+                pendingImage->exptype,
+                pendingImage->class,
+                pendingImage->class_id,
+                pendingImage->p1_version,
+                pendingImage->p2_version,
+                "", // recipe
+                "", // stats
+                pendingImage->url
+            );
+
+            psArrayAdd(doneImages, 100, doneImage);
+        }
+
+        // XXX FIXME
+        p2DoneFrame *doneFrame = p2DoneFrameAlloc(doneExposure, doneImages);
+
+        psArrayAdd(doneFrames, 100, doneFrame);
+    }
+
+    return doneFrames;
+}
+
+// XXX the filename layout is defined by this code
+psArray *p2pendingToDoneImfile(psArray *pendingImfiles)
+{
+    PS_ASSERT_PTR_NON_NULL(pendingImfiles, NULL);
+
+    psArray *doneImfiles = psArrayAlloc(pendingImfiles->n);
+    doneImfiles->n = 0;
+
+    for (int i = 0; i < pendingImfiles->n; i++) {
+        p2PendingImfileRow *pendingImfile = pendingImfiles->data[i];
+
+        p2DoneImfileRow *doneImfile = p2DoneImfileRowAlloc(
+            pendingImfile->exp_id,
+            pendingImfile->exptype,
+            pendingImfile->class,
+            pendingImfile->class_id,
+            pendingImfile->p1_version,
+            pendingImfile->p2_version,
+            "", // recipe
+            "", // stats
+            pendingImfile->url
+        );
+
+        psArrayAdd(doneImfiles, 100, doneImfile);
+    }
+
+    return doneImfiles;
+}
+
+p2DoneExpRow *p2pendingToDoneExp(p2PendingExpRow *pendingExp)
+{
+    PS_ASSERT_PTR_NON_NULL(pendingExp, NULL);
+
+    p2DoneExpRow *doneExp = p2DoneExpRowAlloc(
+        pendingExp->exp_id,
+        pendingExp->camera,
+        pendingExp->filter,
+        pendingExp->class,
+        pendingExp->nclass, // XXX ndone is uneeded?
+        0xFF, // ndone
+        pendingExp->p1_version,
+        pendingExp->p2_version
+    );
+
+    return doneExp;
+}
Index: /tags/rel-0_0_1/ippTools/src/p2rawToPending.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2rawToPending.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2rawToPending.c	(revision 7462)
@@ -0,0 +1,51 @@
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+psArray *p2rawToPending(p2Config *config, psArray *rawFrames) {
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    PS_ASSERT_PTR_NON_NULL(rawFrames, NULL);
+
+    psArray *pendingFrames = psArrayAlloc(rawFrames->n);
+    pendingFrames->n = 0;
+
+    for (int i = 0; i < rawFrames->n; i++) {
+        ppRawFrame *rawFrame = rawFrames->data[i];
+
+        p2PendingExpRow *pendingExposure = p2PendingExpRowAlloc(
+            rawFrame->exposure->exp_id,
+            rawFrame->exposure->camera,
+            rawFrame->exposure->filter,
+            rawFrame->exposure->class,
+            rawFrame->exposure->nclass,
+            0xff,   // p1
+            0xff    // p2
+        );
+
+        psArray *pendingImages = psArrayAlloc(rawFrame->images->n);
+        pendingImages->n = 0;
+        for (int j = 0; j < rawFrame->images->n; j++) {
+            rawImfileRow *rawImage = rawFrame->images->data[j];
+            p2PendingImfileRow *pendingImage = p2PendingImfileRowAlloc(
+                rawImage->exp_id,
+                rawImage->exptype,
+                rawImage->class,
+                rawImage->class_id,
+                0xff,   // p1
+                0xff,   // p2
+                "",     // recipe
+                "",     // stats
+                rawImage->url
+            );
+
+            psArrayAdd(pendingImages, 100, pendingImage);
+        }
+
+        p2PendingFrame *pendingFrame = p2PendingFrameAlloc(pendingExposure,
+            pendingImages);
+        psArrayAdd(pendingFrames, 100, pendingFrame);
+    }
+
+    return pendingFrames;
+}
+
+// XXX the filename layout is defined by this code
Index: /tags/rel-0_0_1/ippTools/src/p2searchDoneFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2searchDoneFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2searchDoneFrames.c	(revision 7462)
@@ -0,0 +1,57 @@
+#include "p2tools.h"
+
+// select raw frames (exposure+images) which match the given config options
+psArray *p2searchDoneFrames(p2Config *config) {
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadata *where = psMetadataAlloc ();
+
+    if (config->camera_name != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "camera", 0, "==",
+            config->camera_name);
+    }
+    if (config->filter != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "filter", 0, "==",
+            config->filter);
+    }
+
+    if (where->list->n < 1) {
+       psFree(where);
+       where = NULL;
+    }
+
+    psArray *exposures = p2DoneExpSelectRowObjects(config->database, where, 
+        MAX_ROWS);
+    psFree (where);
+    if (!exposures) {
+        psError(PS_ERR_UNKNOWN, false, "no p2DoneExp rows found");
+
+        return NULL;
+    }
+
+    // output array of ppRawFrame
+    psArray *frames = psArrayAlloc(exposures->n);
+    frames->n = 0;
+
+    // 'where' to select each exposure
+    where = psMetadataAlloc ();
+    for (int i = 0; i < exposures->n; i++) {
+        p2DoneExpRow *exposure = exposures->data[i];
+
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", PS_META_REPLACE, "==",
+            exposure->exp_id);
+
+        psArray *images = p2DoneImfileSelectRowObjects(config->database, where,
+            MAX_ROWS);
+        if (!images) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+
+            return NULL;
+        }
+
+        p2DoneFrame *doneFrame = p2DoneFrameAlloc(exposure, images);
+        psArrayAdd(frames, 100, doneFrame);
+    }
+
+    return frames;
+} 
Index: /tags/rel-0_0_1/ippTools/src/p2searchPendingFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2searchPendingFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2searchPendingFrames.c	(revision 7462)
@@ -0,0 +1,172 @@
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+psArray *p2searchPendingFrames(p2Config *config) {
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadata *where = psMetadataAlloc();
+
+    if (config->camera_name != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "camera", 0, "==",
+            config->camera_name);
+    }
+    if (config->filter != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "filter", 0, "==",
+            config->filter);
+    }
+    /*
+    if (config->exp_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", 0, "==",
+            config->exp_id);
+    }
+    if (config->class != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class", 0, "==",
+            config->class);
+    }
+    if (config->class_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class_id", 0, "==",
+            config->class_id);
+    }
+    */
+    /*
+    if (!psTimeIsZero(config->start) && !psTimeIsZero(config->stop)) {
+        psMetadataAddTime(where, PS_LIST_TAIL, "TIME_START", 0, "<", config->stop);
+        psMetadataAddTime(where, PS_LIST_TAIL, "TIME_STOP", 0, ">", config->start);
+    }
+    */
+
+    // if no search criteria were added then set where to NULL to search for
+    // everything
+    // XXX fix handling of where
+    if (where->list->n < 1) {
+        psFree(where);
+        where = NULL;
+    }
+
+    psArray *exposures = p2PendingExpSelectRowObjects(config->database, where, MAX_ROWS);
+    psFree(where);
+    if (!exposures) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingExp rows found");
+        return NULL;
+    }
+
+
+    // output array of rawSet
+    psArray *frames = psArrayAlloc(exposures->n);
+    frames->n = 0;
+
+    // 'where' to select each exposure
+    where = psMetadataAlloc();
+    for (int i = 0; i < exposures->n; i++) {
+        p2PendingExpRow *exposure = exposures->data[i];
+
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", PS_META_REPLACE, "==", exposure->exp_id);
+        psMetadataAddS32(where, PS_LIST_TAIL, "p2_version", PS_META_REPLACE, "==", exposure->p2_version);
+
+        psArray *images = p2PendingImfileSelectRowObjects(config->database, where, MAX_ROWS);
+
+        p2PendingFrame *frame = p2PendingFrameAlloc(exposure, images);
+
+        psArrayAdd(frames, 100, frame);
+    }
+
+    return frames;
+} 
+
+// XXX EAM : this may be more efficient if we just select all p2PendingImages
+// which have the appropriate state.  that would limit the ability to filter
+// the selection.  test to see how well/poorly this performs
+
+psArray *p2searchPendingImfiles(p2Config *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadata *where = psMetadataAlloc();
+    if (config->camera_name != NULL) {
+        if (!psMetadataAddStr(where, PS_LIST_TAIL, "camera", 0, "==",
+            config->camera_name)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+            psFree(where);
+            return NULL;
+        }
+    }
+    if (config->filter != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "filter", 0, "==",
+            config->filter);
+    }
+    if (config->exp_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", 0, "==",
+            config->exp_id);
+    }
+    if (config->class != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class", 0, "==",
+            config->class);
+    }
+    if (config->class_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class_id", 0, "==",
+            config->class_id);
+    }
+
+    if (where->list->n < 1) {
+        psFree(where);
+        where = NULL;
+    }
+
+    psArray *imfiles = p2PendingImfileSelectRowObjects(config->database, where,
+        MAX_ROWS);
+    psFree(where);
+    if (!imfiles) {
+        psError(PS_ERR_UNKNOWN, false, "no rawScienceExp rows found");
+
+        return NULL;
+    }
+
+    return imfiles;
+}
+
+psArray *p2searchPendingExp(p2Config *config)
+{
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadata *where = psMetadataAlloc();
+    if (config->camera_name != NULL) {
+        if (!psMetadataAddStr(where, PS_LIST_TAIL, "camera", 0, "==",
+            config->camera_name)) {
+            psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+            psFree(where);
+            return NULL;
+        }
+    }
+    if (config->filter != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "filter", 0, "==",
+            config->filter);
+    }
+    if (config->exp_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", 0, "==",
+            config->exp_id);
+    }
+    if (config->class != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class", 0, "==",
+            config->class);
+    }
+    if (config->class_id != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "class_id", 0, "==",
+            config->class_id);
+    }
+
+    if (where->list->n < 1) {
+        psFree(where);
+        where = NULL;
+    }
+
+    psArray *exps = p2PendingExpSelectRowObjects(config->database, where,
+        MAX_ROWS);
+    psFree(where);
+    if (!exps) {
+        psError(PS_ERR_UNKNOWN, false, "no p2PendingExp rows found");
+
+        return NULL;
+    }
+
+    return exps;
+}
Index: /tags/rel-0_0_1/ippTools/src/p2searchRawFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2searchRawFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2searchRawFrames.c	(revision 7462)
@@ -0,0 +1,67 @@
+# include "p2tools.h"
+
+// select raw frames (exposure+images) which match the given config options
+psArray *p2searchRawFrames(p2Config *config) {
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    psMetadata *where = psMetadataAlloc();
+   
+    if (config->camera_name != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "camera", 0, "==",
+            config->camera_name);
+    }
+    if (config->filter != NULL) {
+        psMetadataAddStr(where, PS_LIST_TAIL, "filter", 0, "==",
+            config->filter);
+    }
+    // psMetadataConfig does not support times yet
+    /*
+    if (!psTimeIsZero(config->start) && !psTimeIsZero(config->stop)) {
+        psMetadataAddTime (where, PS_LIST_TAIL, "TIME_START", 0, "<", config->stop);
+        psMetadataAddTime (where, PS_LIST_TAIL, "TIME_STOP", 0, ">", config->start);
+    }
+    */
+
+    // if no search criteria were added then set where to NULL to search for
+    // everything
+    // XXX fix handling of where
+    if (where->list->n < 1) {
+        psFree(where);
+        where = NULL;
+    }
+
+    psArray *exposures = rawScienceExpSelectRowObjects(config->database, where, 
+        MAX_ROWS);
+    psFree(where);
+    if (!exposures) {
+        psError(PS_ERR_UNKNOWN, false, "no rawScienceExp rows found");
+
+        return NULL;
+    }
+
+    // output array of ppRawFrame
+    psArray *frames = psArrayAlloc(exposures->n);
+    frames->n = 0;
+
+    // 'where' to select each exposure
+    where = psMetadataAlloc ();
+    for (int i = 0; i < exposures->n; i++) {
+        rawScienceExpRow *exposure = exposures->data[i];
+
+        psMetadataAddStr(where, PS_LIST_TAIL, "exp_id", PS_META_REPLACE, "==",
+            exposure->exp_id);
+
+        psArray *images = rawImfileSelectRowObjects(config->database, where,
+            MAX_ROWS);
+        if (!images) {
+            psError(PS_ERR_UNKNOWN, false, "database access failed");
+
+            return NULL;
+        }
+
+        ppRawFrame *rawFrame = ppRawFrameAlloc(exposure, images);
+        psArrayAdd(frames, 100, rawFrame);
+    }
+
+    return frames;
+} 
Index: /tags/rel-0_0_1/ippTools/src/p2updatePending.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2updatePending.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2updatePending.c	(revision 7462)
@@ -0,0 +1,33 @@
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are not done, select their done images, count, update
+bool p2updatePendingFrames(p2Config *config, psArray *pendingFrames) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(pendingFrames, false);
+
+    psMetadata *where = psMetadataAlloc ();
+
+    // select all of the exposures which are still pending
+    psMetadataAddS32 (where, PS_LIST_TAIL, "STATE", 0, "==", P2_MODE_PENDING);
+    psArray *exposures = p2PendingExpSelectRowObjects (config->database, where, MAX_ROWS);
+
+    // we will now select all of the matching images which are done
+    psMetadataAddS32 (where, PS_LIST_TAIL, "STATE", PS_META_REPLACE, "==", P2_MODE_DONE);
+    for (int i = 0; i < exposures->n; i++) {
+	p2PendingExpRow *exposure = exposures->data[i];
+
+	// find the next available version numbers
+	psMetadataAddStr (where, PS_LIST_TAIL, "EXP_ID",     PS_META_REPLACE, "==", exposure->exp_id);
+	psMetadataAddS32 (where, PS_LIST_TAIL, "P2_VERSION", PS_META_REPLACE, "==", exposure->p2_version);
+	psArray *images = p2PendingImfileSelectRowObjects (config->database, where, MAX_ROWS);
+	if (images->n != exposure->nclass) {
+	    // free things
+	    continue;
+	}
+	// frame is complete, set exposure state to done
+	// XXX add P2 stats here?
+//	exposure->state = P2_MODE_DONE;
+//	p2PendingExposureUpdateRows (config->database, exposure);
+    }
+    return true;
+} 
Index: /tags/rel-0_0_1/ippTools/src/p2writePendingFrames.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/p2writePendingFrames.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/p2writePendingFrames.c	(revision 7462)
@@ -0,0 +1,27 @@
+# include "p2tools.h"
+
+// select pending frames (exposure+images) which are done/not done
+bool p2writePendingFrames (p2Config *config, psArray *frames) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PS_ASSERT_PTR_NON_NULL(frames, false);
+
+    fprintf (stdout, "# exp_id class class_id url\n");
+
+    for (int i = 0; i < frames->n; i++) {
+        p2PendingFrame *frame = frames->data[i];
+        PS_ASSERT_PTR_NON_NULL(frame, false);
+        PS_ASSERT_PTR_NON_NULL(frame->images, false);
+
+        for (int j = 0; j < frame->images->n; j++) {
+            p2PendingImfileRow *image = frame->images->data[j];
+            fprintf (stdout, "%s %s %s %s\n",
+                image->exp_id,
+                image->class,
+                image->class_id,
+                image->url
+            );
+        }
+    }
+
+    return true;
+} 
Index: /tags/rel-0_0_1/ippTools/src/pxadmin.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/pxadmin.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/pxadmin.c	(revision 7462)
@@ -0,0 +1,35 @@
+#include <stdlib.h>
+#include <metadatadb.h>
+
+#include "p2tools.h"
+
+int main(int argc, char **argv) {
+    p2Config        config;
+
+    p2adminConfig(&config, argc, argv);
+
+    switch (config.mode) {
+        case P2_MODE_RECREATE:
+            if (!p2deleteTables(&config)) {
+                psError(PS_ERR_UNKNOWN, false, "table deletion failed");
+                exit(EXIT_FAILURE);
+            }
+            // fall through
+        case P2_MODE_CREATE:
+            if (!p2createTables(&config)) {
+                psError(PS_ERR_UNKNOWN, false, "table creation failed");
+                exit(EXIT_FAILURE);
+            }
+            break;
+        case P2_MODE_DELETE:
+            if (!p2deleteTables(&config)) {
+                psError(PS_ERR_UNKNOWN, false, "table deletion failed");
+                exit(EXIT_FAILURE);
+            }
+            break;
+        default:
+            psAbort(argv[0], "invalid option (this should not happen)");
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippTools/src/pxadminConfig.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/pxadminConfig.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/pxadminConfig.c	(revision 7462)
@@ -0,0 +1,82 @@
+# include <pmConfig.h>
+# include "p2tools.h"
+
+bool p2adminConfig(p2Config *config, int argc, char **argv) {
+    PS_ASSERT_PTR_NON_NULL(config, false);
+
+    // Parse the configurations (re-org a la ppImage)
+    config->site = NULL;            // Site configuration
+    config->camera = NULL;          // Camera configuration
+    config->recipe = NULL;          // Recipe configuration
+    if (! pmConfigRead(&config->site, &config->camera, &config->recipe, &argc, argv, RECIPE)) {
+        psErrorStackPrint(stderr, "Can't find site configuration!\n");
+        exit(EXIT_FAILURE);
+    }
+
+    // Parse other command-line arguments
+    config->arguments = psMetadataAlloc(); // The arguments, with default values
+
+    int N;
+    config->mode = P2_MODE_NONE;
+    if ((N = psArgumentGet(argc, argv, "-create"))) {
+        psArgumentRemove(N, &argc, argv);
+        if (config->mode) {
+            psAbort(argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_CREATE;
+    }
+    if ((N = psArgumentGet(argc, argv, "-delete"))) {
+        psArgumentRemove(N, &argc, argv);
+        if (config->mode) {
+            psAbort(argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_DELETE;
+    }
+    if ((N = psArgumentGet(argc, argv, "-recreate"))) {
+        psArgumentRemove(N, &argc, argv);
+        if (config->mode) {
+            psAbort(argv[0], "only one mode selection is allowed");
+        }
+        config->mode = P2_MODE_RECREATE;
+    }
+
+    // paul's argument parsing convention requires: -key value 
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-create", 0,
+            "create the P2 tables", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-delete", 0,
+            "delete the P2 tables", "");
+    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "-recreate", 0,
+            "delete and recreate the P2 tables", "");
+
+    if (config->mode == P2_MODE_NONE) {
+        fprintf (stderr, "admin mode not specified\n");
+        psArgumentHelp(config->arguments);
+        psFree(config->arguments);
+        exit(EXIT_FAILURE);
+    }
+
+    if ((N = psArgumentGet (argc, argv, "-help"))) {
+        psArgumentHelp(config->arguments);
+        psFree(config->arguments);
+        exit(EXIT_FAILURE);
+    }
+
+    if (! psArgumentParse(config->arguments, &argc, argv) || argc != 1) {
+        printf("\nPan-STARRS Phase 2 Admin Tool\n\n");
+        printf("Usage: %s [mode]\n", argv[0]);
+        printf(" [mode] : -create | -delete\n\n");
+        psArgumentHelp(config->arguments);
+        psFree(config->arguments);
+        exit(EXIT_FAILURE);
+    }
+
+    // define Database handle, if used
+    config->database = pmConfigDB(config->site);
+    return true;
+} 
+
+/*
+ * USAGE:
+ * p2admin -create : create the P2 tables
+ * p2admin -delete : delete the P2 tables (asks for confirmation)
+ */
Index: /tags/rel-0_0_1/ippTools/src/pxalloc.c
===================================================================
--- /tags/rel-0_0_1/ippTools/src/pxalloc.c	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/pxalloc.c	(revision 7462)
@@ -0,0 +1,90 @@
+#include <pslib.h>
+
+#include "p2tools.h"
+
+static void ppRawFrameFree(ppRawFrame *ptr);
+static void p2PendingFrameFree(p2PendingFrame *ptr);
+static void p2DoneFrameFree(p2DoneFrame *ptr);
+
+ppRawFrame *ppRawFrameAlloc(
+    rawScienceExpRow *exposure,
+    psArray *images
+)
+{
+    ppRawFrame *frame;
+
+    PS_ASSERT_PTR_NON_NULL(exposure, NULL);
+    PS_ASSERT_PTR_NON_NULL(images, NULL);
+
+    frame = psAlloc(sizeof(ppRawFrame));
+    psMemSetDeallocator(frame, (psFreeFunc)ppRawFrameFree);
+
+    frame->exposure = exposure;
+    frame->images   = images;
+
+    return frame;
+}
+
+static void ppRawFrameFree(ppRawFrame *ptr)
+{
+    psFree(ptr->exposure);
+    psFree(ptr->images);
+}
+
+p2PendingFrame *p2PendingFrameAlloc(
+    p2PendingExpRow *exposure,
+    psArray *images
+)
+{
+    p2PendingFrame *frame;
+
+    PS_ASSERT_PTR_NON_NULL(exposure, NULL);
+    PS_ASSERT_PTR_NON_NULL(images, NULL);
+
+    frame = psAlloc(sizeof(p2PendingFrame));
+    psMemSetDeallocator(frame, (psFreeFunc)p2PendingFrameFree);
+
+    frame->exposure = exposure;
+    frame->images   = images;
+
+    return frame;
+}
+
+static void p2PendingFrameFree(p2PendingFrame *ptr)
+{
+    psFree(ptr->exposure);
+    psFree(ptr->images);
+}
+
+p2DoneFrame *p2DoneFrameAlloc(
+    p2DoneExpRow *exposure,
+    psArray *images
+)
+{
+    p2DoneFrame *frame;
+
+    PS_ASSERT_PTR_NON_NULL(exposure, NULL);
+    PS_ASSERT_PTR_NON_NULL(images, NULL);
+
+    frame = psAlloc(sizeof(p2DoneFrame));
+    psMemSetDeallocator(frame, (psFreeFunc)p2DoneFrameFree);
+
+    frame->exposure = exposure;
+    frame->images   = images;
+
+    return frame;
+}
+
+static void p2DoneFrameFree(p2DoneFrame *ptr)
+{
+    psFree(ptr->exposure);
+    psFree(ptr->images);
+}
+
+bool psTimeIsZero(psTime *time) {
+    if (time->sec == 0 || time->nsec == 0) {
+        return false;
+    }
+
+    return true;
+}
Index: /tags/rel-0_0_1/ippTools/src/pxtools.h
===================================================================
--- /tags/rel-0_0_1/ippTools/src/pxtools.h	(revision 7462)
+++ /tags/rel-0_0_1/ippTools/src/pxtools.h	(revision 7462)
@@ -0,0 +1,96 @@
+# include <stdio.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include <pmObjects.h>
+# include <pmPSF.h>
+# include <pmPSFtry.h>
+# include <pmModelGroup.h>
+# include <metadatadb.h>
+
+// load these values from the db in the init stage
+# define P2_TYPE_OBJECT 1
+# define P2_STATE_READY 2
+# define RECIPE "PHASE2"
+# define MAX_ROWS 10e9
+
+typedef enum {
+    P2_MODE_NONE,		      	// grab from raw, output for ppImage
+    P2_MODE_QUICK,		      	// grab from raw, output for ppImage
+    P2_MODE_DEFINE,			// grab from raw, create pending
+    P2_MODE_PENDING,			// grab from pending, output for ppImage
+//    P2_MODE_UPDATE,			// set the current state
+    P2_MODE_DONE,			// set the current state
+    P2_MODE_CREATE,			// set the current state
+    P2_MODE_DELETE,			// set the current state
+    P2_MODE_RECREATE,		// set the current state
+} p2mode;
+
+typedef struct {
+    p2mode mode;
+    psTime *start;
+    psTime *stop;
+    psSphere r1;
+    psSphere r2;
+    psMetadata *camera;
+    char *camera_name;
+    char *filter;
+    psDB *database;
+    psMetadata *site;
+    psMetadata *recipe;
+    psMetadata *arguments;
+    char *exp_id;
+    char *class;
+    char *class_id;
+    char *url;
+} p2Config;
+
+typedef struct {
+    rawScienceExpRow *exposure;
+    psArray *images;
+} ppRawFrame;
+
+ppRawFrame *ppRawFrameAlloc(
+    rawScienceExpRow *exposure,
+    psArray *images
+);
+
+typedef struct {
+    p2PendingExpRow *exposure;
+    psArray *images;
+} p2PendingFrame;
+
+p2PendingFrame *p2PendingFrameAlloc(
+    p2PendingExpRow *exposure,
+    psArray *images
+);
+
+typedef struct {
+    p2DoneExpRow *exposure;
+    psArray *images;
+} p2DoneFrame;
+
+p2DoneFrame *p2DoneFrameAlloc(
+    p2DoneExpRow *exposure,
+    psArray *images
+);
+
+bool p2createTables (p2Config *config);
+bool p2deleteTables (p2Config *config);
+bool p2adminConfig (p2Config *config, int argc, char **argv);
+bool p2searchConfig (p2Config *config, int argc, char **argv);
+psArray *p2rawToPending (p2Config *config, psArray *rawFrames);
+psArray *p2searchRawFrames (p2Config *config);
+bool p2insertPendingFrames (p2Config *config, psArray *rawFrames);
+psArray *p2searchPendingFrames (p2Config *config);
+bool p2writePendingImages (p2Config *config, psArray *frames);
+bool p2updatePendingFrames (p2Config *config, psArray *pendingFrames);
+psArray *p2pendingToDone (p2Config *config, psArray *pendingFrames);
+bool p2insertDoneFrames (p2Config *config, psArray *doneFrames);
+bool psTimeIsZero(psTime *time);
+bool p2writePendingFrames (p2Config *config, psArray *frames);
+psArray *p2searchDoneFrames(p2Config *config);
+psArray *p2searchPendingImfiles(p2Config *config);
+psArray *p2pendingToDoneImfile(psArray *pendingImfiles);
+psArray *p2searchPendingExp(p2Config *config);
+p2DoneExpRow *p2pendingToDoneExp(p2PendingExpRow *pendingExp);
Index: /tags/rel-0_0_1/ippdb/Doxyfile.in
===================================================================
--- /tags/rel-0_0_1/ippdb/Doxyfile.in	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/Doxyfile.in	(revision 7462)
@@ -0,0 +1,1219 @@
+# Doxyfile 1.4.2
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+#       TAG = value [value, ...]
+# For lists items can also be appended using:
+#       TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded 
+# by quotes) that should identify the project.
+
+PROJECT_NAME           = @PACKAGE_NAME@
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. 
+# This could be handy for archiving the generated documentation or 
+# if some version control system is used.
+
+PROJECT_NUMBER         = @PACKAGE_VERSION@
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) 
+# base path where the generated documentation will be put. 
+# If a relative path is entered, it will be relative to the location 
+# where doxygen was started. If left blank the current directory will be used.
+
+# @top_builddir@ doesn't work for some reason
+OUTPUT_DIRECTORY       = @builddir@/docs
+
+# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 
+# 4096 sub-directories (in 2 levels) under the output directory of each output 
+# format and will distribute the generated files over these directories. 
+# Enabling this option can be useful when feeding doxygen a huge amount of 
+# source files, where putting all generated files in the same directory would 
+# otherwise cause performance problems for the file system.
+
+CREATE_SUBDIRS         = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all 
+# documentation generated by doxygen is written. Doxygen will use this 
+# information to generate all constant output in the proper language. 
+# The default language is English, other supported languages are: 
+# Brazilian, Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, 
+# Dutch, Finnish, French, German, Greek, Hungarian, Italian, Japanese, 
+# Japanese-en (Japanese with English messages), Korean, Korean-en, Norwegian, 
+# Polish, Portuguese, Romanian, Russian, Serbian, Slovak, Slovene, Spanish, 
+# Swedish, and Ukrainian.
+
+OUTPUT_LANGUAGE        = English
+
+# This tag can be used to specify the encoding used in the generated output. 
+# The encoding is not always determined by the language that is chosen, 
+# but also whether or not the output is meant for Windows or non-Windows users. 
+# In case there is a difference, setting the USE_WINDOWS_ENCODING tag to YES 
+# forces the Windows encoding (this is the default for the Windows binary), 
+# whereas setting the tag to NO uses a Unix-style encoding (the default for 
+# all platforms other than Windows).
+
+USE_WINDOWS_ENCODING   = NO
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will 
+# include brief member descriptions after the members that are listed in 
+# the file and class documentation (similar to JavaDoc). 
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend 
+# the brief description of a member or function before the detailed description. 
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the 
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator 
+# that is used to form the text in various listings. Each string 
+# in this list, if found as the leading text of the brief description, will be 
+# stripped from the text and the result after processing the whole list, is 
+# used as the annotated text. Otherwise, the brief description is used as-is. 
+# If left blank, the following values are used ("$name" is automatically 
+# replaced with the name of the entity): "The $name class" "The $name widget" 
+# "The $name file" "is" "provides" "specifies" "contains" 
+# "represents" "a" "an" "the"
+
+ABBREVIATE_BRIEF       = 
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then 
+# Doxygen will generate a detailed section even if there is only a brief 
+# description.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all 
+# inherited members of a class in the documentation of that class as if those 
+# members were ordinary class members. Constructors, destructors and assignment 
+# operators of the base classes will not be shown.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full 
+# path before files name in the file list and in the header files. If set 
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag 
+# can be used to strip a user-defined part of the path. Stripping is 
+# only done if one of the specified strings matches the left-hand part of 
+# the path. The tag can be used to show relative paths in the file list. 
+# If left blank the directory from which doxygen is run is used as the 
+# path to strip.
+
+STRIP_FROM_PATH        = 
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of 
+# the path mentioned in the documentation of a class, which tells 
+# the reader which header file to include in order to use a class. 
+# If left blank only the name of the header file containing the class 
+# definition is used. Otherwise one should specify the include paths that 
+# are normally passed to the compiler using the -I flag.
+
+STRIP_FROM_INC_PATH    = 
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter 
+# (but less readable) file names. This can be useful is your file systems 
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen 
+# will interpret the first line (until the first dot) of a JavaDoc-style 
+# comment as the brief description. If set to NO, the JavaDoc 
+# comments will behave just like the Qt-style comments (thus requiring an 
+# explicit @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make Doxygen 
+# treat a multi-line C++ special comment block (i.e. a block of //! or /// 
+# comments) as a brief description. This used to be the default behaviour. 
+# The new default is to treat a multi-line C++ comment block as a detailed 
+# description. Set this tag to YES if you prefer the old behaviour instead.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# If the DETAILS_AT_TOP tag is set to YES then Doxygen 
+# will output the detailed description near the top, like JavaDoc.
+# If set to NO, the detailed description appears after the member 
+# documentation.
+
+DETAILS_AT_TOP         = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented 
+# member inherits the documentation from any documented member that it 
+# re-implements.
+
+INHERIT_DOCS           = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC 
+# tag is set to YES, then doxygen will reuse the documentation of the first 
+# member in the group (if any) for the other members of the group. By default 
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce 
+# a new page for each member. If set to NO, the documentation of a member will 
+# be part of the file/class/namespace that contains it.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. 
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE               = 4
+
+# This tag can be used to specify a number of aliases that acts 
+# as commands in the documentation. An alias has the form "name=value". 
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to 
+# put the command \sideeffect (or @sideeffect) in the documentation, which 
+# will result in a user-defined paragraph with heading "Side Effects:". 
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES                = 
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C 
+# sources only. Doxygen will then generate output that is more tailored for C. 
+# For instance, some of the names that are used will be different. The list 
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources 
+# only. Doxygen will then generate output that is more tailored for Java. 
+# For instance, namespaces will be presented as packages, qualified scopes 
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the SUBGROUPING tag to YES (the default) to allow class member groups of 
+# the same type (for instance a group of public functions) to be put as a 
+# subgroup of that type (e.g. under the Public Functions section). Set it to 
+# NO to prevent subgrouping. Alternatively, this can be done per class using 
+# the \nosubgrouping command.
+
+SUBGROUPING            = YES
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in 
+# documentation are documented, even if no documentation was available. 
+# Private class members and static file members will be hidden unless 
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class 
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file 
+# will be included in the documentation.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) 
+# defined locally in source files will be included in the documentation. 
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. When set to YES local 
+# methods, which are defined in the implementation section but not in 
+# the interface are included in the documentation. 
+# If set to NO (the default) only methods in the interface are included.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all 
+# undocumented members of documented classes, files or namespaces. 
+# If set to NO (the default) these members will be included in the 
+# various overviews, but no documentation section is generated. 
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all 
+# undocumented classes that are normally visible in the class hierarchy. 
+# If set to NO (the default) these classes will be included in the various 
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, Doxygen will hide all 
+# friend (class|struct|union) declarations. 
+# If set to NO (the default) these declarations will be included in the 
+# documentation.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, Doxygen will hide any 
+# documentation blocks found inside the body of a function. 
+# If set to NO (the default) these blocks will be appended to the 
+# function's detailed documentation block.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation 
+# that is typed after a \internal command is included. If the tag is set 
+# to NO (the default) then the documentation will be excluded. 
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate 
+# file names in lower-case letters. If set to YES upper-case letters are also 
+# allowed. This is useful if you have classes or files whose names only differ 
+# in case and if your file system supports case sensitive file names. Windows 
+# and Mac users are advised to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen 
+# will show members with their full class and namespace scopes in the 
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen 
+# will put a list of the files that are included by a file in the documentation 
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] 
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen 
+# will sort the (detailed) documentation of file and class members 
+# alphabetically by member name. If set to NO the members will appear in 
+# declaration order.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the 
+# brief documentation of file, namespace and class members alphabetically 
+# by member name. If set to NO (the default) the members will appear in 
+# declaration order.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be 
+# sorted by fully-qualified names, including namespaces. If set to 
+# NO (the default), the class list will be sorted only by class name, 
+# not including the namespace part. 
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the 
+# alphabetical list.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or 
+# disable (NO) the todo list. This list is created by putting \todo 
+# commands in the documentation.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or 
+# disable (NO) the test list. This list is created by putting \test 
+# commands in the documentation.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or 
+# disable (NO) the bug list. This list is created by putting \bug 
+# commands in the documentation.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or 
+# disable (NO) the deprecated list. This list is created by putting 
+# \deprecated commands in the documentation.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional 
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       = 
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines 
+# the initial value of a variable or define consists of for it to appear in 
+# the documentation. If the initializer consists of more lines than specified 
+# here it will be hidden. Use a value of 0 to hide initializers completely. 
+# The appearance of the initializer of individual variables and defines in the 
+# documentation can be controlled using \showinitializer or \hideinitializer 
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated 
+# at the bottom of the documentation of classes and structs. If set to YES the 
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES        = YES
+
+# If the sources in your project are distributed over multiple directories 
+# then setting the SHOW_DIRECTORIES tag to YES will show the directory hierarchy 
+# in the documentation.
+
+SHOW_DIRECTORIES       = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that 
+# doxygen should invoke to get the current version for each file (typically from the 
+# version control system). Doxygen will invoke the program by executing (via 
+# popen()) the command <command> <input-file>, where <command> is the value of 
+# the FILE_VERSION_FILTER tag, and <input-file> is the name of an input file 
+# provided by doxygen. Whatever the progam writes to standard output 
+# is used as the file version. See the manual for examples.
+
+FILE_VERSION_FILTER    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated 
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are 
+# generated by doxygen. Possible values are YES and NO. If left blank 
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings 
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will 
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If WARN_IF_DOC_ERROR is set to YES, doxygen will generate warnings for 
+# potential errors in the documentation, such as not documenting some 
+# parameters in a documented function, or documenting parameters that 
+# don't exist or using markup commands wrongly.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be abled to get warnings for 
+# functions that are documented, but have no documentation for their parameters 
+# or return value. If set to NO (the default) doxygen will only warn about 
+# wrong or incomplete parameter documentation, but not about the absence of 
+# documentation.
+
+WARN_NO_PARAMDOC       = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that 
+# doxygen can produce. The string should contain the $file, $line, and $text 
+# tags, which will be replaced by the file and line number from which the 
+# warning originated and the warning text. Optionally the format may contain 
+# $version, which will be replaced by the version of the file (if it could 
+# be obtained via FILE_VERSION_FILTER)
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning 
+# and error messages should be written. If left blank the output is written 
+# to stderr.
+
+WARN_LOGFILE           = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain 
+# documented source files. You may enter file names like "myfile.cpp" or 
+# directories like "/usr/src/myproject". Separate the files or directories 
+# with spaces.
+
+INPUT                  = @top_srcdir@/src
+
+# If the value of the INPUT tag contains directories, you can use the 
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp 
+# and *.h) to filter out the source-files in the directories. If left 
+# blank the following patterns are tested: 
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx 
+# *.hpp *.h++ *.idl *.odl *.cs *.php *.php3 *.inc *.m *.mm
+
+FILE_PATTERNS          = *.h
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories 
+# should be searched for input files as well. Possible values are YES and NO. 
+# If left blank NO is used.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should 
+# excluded from the INPUT source files. This way you can easily exclude a 
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE                = 
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or 
+# directories that are symbolic links (a Unix filesystem feature) are excluded 
+# from the input.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the 
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude 
+# certain files from those directories.
+
+EXCLUDE_PATTERNS       = 
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or 
+# directories that contain example code fragments that are included (see 
+# the \include command).
+
+EXAMPLE_PATH           = 
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the 
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp 
+# and *.h) to filter out the source-files in the directories. If left 
+# blank all files are included.
+
+EXAMPLE_PATTERNS       = 
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be 
+# searched for input files to be used with the \include or \dontinclude 
+# commands irrespective of the value of the RECURSIVE tag. 
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or 
+# directories that contain image that are included in the documentation (see 
+# the \image command).
+
+IMAGE_PATH             = 
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should 
+# invoke to filter for each input file. Doxygen will invoke the filter program 
+# by executing (via popen()) the command <filter> <input-file>, where <filter> 
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an 
+# input file. Doxygen will then use the output that the filter program writes 
+# to standard output.  If FILTER_PATTERNS is specified, this tag will be 
+# ignored.
+
+INPUT_FILTER           = 
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern 
+# basis.  Doxygen will compare the file name with each pattern and apply the 
+# filter if there is a match.  The filters are a list of the form: 
+# pattern=filter (like *.cpp=my_cpp_filter). See INPUT_FILTER for further 
+# info on how filters are used. If FILTER_PATTERNS is empty, INPUT_FILTER 
+# is applied to all files.
+
+FILTER_PATTERNS        = 
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using 
+# INPUT_FILTER) will be used to filter the input files when producing source 
+# files to browse (i.e. when SOURCE_BROWSER is set to YES).
+
+FILTER_SOURCE_FILES    = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will 
+# be generated. Documented entities will be cross-referenced with these sources. 
+# Note: To get rid of all source code in the generated output, make sure also 
+# VERBATIM_HEADERS is set to NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body 
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct 
+# doxygen to hide any special comment blocks from generated source code 
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default) 
+# then for each documented function all documented 
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES (the default) 
+# then for each documented function all documented entities 
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = YES
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen 
+# will generate a verbatim copy of the header file for each class for 
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS       = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index 
+# of all compounds will be generated. Enable this if the project 
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX     = NO
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then 
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns 
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# In case all classes in a project start with a common prefix, all 
+# classes will be put under the same header in the alphabetical index. 
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that 
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will 
+# generate HTML output.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for 
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank 
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard header.
+
+HTML_HEADER            = 
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard footer.
+
+HTML_FOOTER            = 
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading 
+# style sheet that is used by each HTML page. It can be used to 
+# fine-tune the look of the HTML output. If the tag is left blank doxygen 
+# will generate a default style sheet. Note that doxygen will try to copy 
+# the style sheet file to the HTML output directory, so don't put your own 
+# stylesheet in the HTML output directory as well, or it will be erased!
+
+HTML_STYLESHEET        = 
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, 
+# files or namespaces will be aligned in HTML using tables. If set to 
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS     = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files 
+# will be generated that can be used as input for tools like the 
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) 
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the CHM_FILE tag can 
+# be used to specify the file name of the resulting .chm file. You 
+# can add a path in front of the file if the result should not be 
+# written to the html output directory.
+
+CHM_FILE               = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the HHC_LOCATION tag can 
+# be used to specify the location (absolute path including file name) of 
+# the HTML help compiler (hhc.exe). If non-empty doxygen will try to run 
+# the HTML help compiler on the generated index.hhp.
+
+HHC_LOCATION           = 
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag 
+# controls if a separate .chi index file is generated (YES) or that 
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag 
+# controls whether a binary table of contents is generated (YES) or a 
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members 
+# to the contents of the HTML help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at 
+# top of each HTML page. The value NO (the default) enables the index and 
+# the value YES disables it.
+
+DISABLE_INDEX          = NO
+
+# This tag can be used to set the number of enum values (range [1..20]) 
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be
+# generated containing a tree-like index structure (just like the one that 
+# is generated for HTML Help). For this to work a browser that supports 
+# JavaScript, DHTML, CSS and frames is required (for instance Mozilla 1.0+, 
+# Netscape 6.0+, Internet explorer 5.0+, or Konqueror). Windows users are 
+# probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW      = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be 
+# used to set the initial width (in pixels) of the frame in which the tree 
+# is shown.
+
+TREEVIEW_WIDTH         = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will 
+# generate Latex output.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be 
+# invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to 
+# generate index for LaTeX. If left blank `makeindex' will be used as the 
+# default command name.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact 
+# LaTeX documents. This may be useful for small projects and may help to 
+# save some trees in general.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used 
+# by the printer. Possible values are: a4, a4wide, letter, legal and 
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX 
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES         = 
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for 
+# the generated latex document. The header should contain everything until 
+# the first chapter. If it is left blank doxygen will generate a 
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           = 
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated 
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will 
+# contain links (just like the HTML output) instead of page references 
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS         = NO
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of 
+# plain latex in the generated Makefile. Set this option to YES to get a 
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode. 
+# command to the generated LaTeX files. This will instruct LaTeX to keep 
+# running if errors occur, instead of asking the user for help. 
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE        = NO
+
+# If LATEX_HIDE_INDICES is set to YES then doxygen will not 
+# include the index chapters (such as File Index, Compound Index, etc.) 
+# in the output.
+
+LATEX_HIDE_INDICES     = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output 
+# The RTF output is optimized for Word 97 and may not look very pretty with 
+# other RTF readers or editors.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact 
+# RTF documents. This may be useful for small projects and may help to 
+# save some trees in general.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated 
+# will contain hyperlink fields. The RTF file will 
+# contain links (just like the HTML output) instead of page references. 
+# This makes the output suitable for online browsing using WORD or other 
+# programs which support those fields. 
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's 
+# config file, i.e. a series of assignments. You only have to provide 
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    = 
+
+# Set optional variables used in the generation of an rtf document. 
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will 
+# generate man pages
+
+GENERATE_MAN           = YES
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to 
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output, 
+# then it will generate one additional man file for each entity 
+# documented in the real man page(s). These additional files 
+# only source the real man page, but without them the man command 
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will 
+# generate an XML file that captures the structure of 
+# the code including all documentation.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. 
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be 
+# put in front of it. If left blank `xml' will be used as the default path.
+
+XML_OUTPUT             = xml
+
+# The XML_SCHEMA tag can be used to specify an XML schema, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_SCHEMA             = 
+
+# The XML_DTD tag can be used to specify an XML DTD, 
+# which can be used by a validating XML parser to check the 
+# syntax of the XML files.
+
+XML_DTD                = 
+
+# If the XML_PROGRAMLISTING tag is set to YES Doxygen will 
+# dump the program listings (including syntax highlighting 
+# and cross-referencing information) to the XML output. Note that 
+# enabling this will significantly increase the size of the XML output.
+
+XML_PROGRAMLISTING     = YES
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will 
+# generate an AutoGen Definitions (see autogen.sf.net) file 
+# that captures the structure of the code including all 
+# documentation. Note that this feature is still experimental 
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES Doxygen will 
+# generate a Perl module file that captures the structure of 
+# the code including all documentation. Note that this 
+# feature is still experimental and incomplete at the 
+# moment.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES Doxygen will generate 
+# the necessary Makefile rules, Perl scripts and LaTeX code to be able 
+# to generate PDF and DVI output from the Perl module output.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be 
+# nicely formatted so it can be parsed by a human reader.  This is useful 
+# if you want to understand what is going on.  On the other hand, if this 
+# tag is set to NO the size of the Perl module output will be much smaller 
+# and Perl will parse it just the same.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file 
+# are prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. 
+# This is useful so different doxyrules.make files included by the same 
+# Makefile don't overwrite each other's variables.
+
+PERLMOD_MAKEVAR_PREFIX = 
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor   
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will 
+# evaluate all C-preprocessor directives found in the sources and include 
+# files.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro 
+# names in the source code. If set to NO (the default) only conditional 
+# compilation will be performed. Macro expansion can be done in a controlled 
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES 
+# then the macro expansion is limited to the macros specified with the 
+# PREDEFINED and EXPAND_AS_PREDEFINED tags.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files 
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that 
+# contain include files that are not input files but should be processed by 
+# the preprocessor.
+
+INCLUDE_PATH           = 
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard 
+# patterns (like *.h and *.hpp) to filter out the header-files in the 
+# directories. If left blank, the patterns specified with FILE_PATTERNS will 
+# be used.
+
+INCLUDE_FILE_PATTERNS  = 
+
+# The PREDEFINED tag can be used to specify one or more macro names that 
+# are defined before the preprocessor is started (similar to the -D option of 
+# gcc). The argument of the tag is a list of macros of the form: name 
+# or name=definition (no spaces). If the definition and the = are 
+# omitted =1 is assumed. To prevent a macro definition from being 
+# undefined via #undef or recursively expanded use the := operator 
+# instead of the = operator.
+
+PREDEFINED             = 
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then 
+# this tag can be used to specify a list of macro names that should be expanded. 
+# The macro definition that is found in the sources will be used. 
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED      = 
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then 
+# doxygen's preprocessor will remove all function-like macros that are alone 
+# on a line, have an all uppercase name, and do not end with a semicolon. Such 
+# function macros are typically used for boiler-plate code, and will confuse 
+# the parser if not removed.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to external references   
+#---------------------------------------------------------------------------
+
+# The TAGFILES option can be used to specify one or more tagfiles. 
+# Optionally an initial location of the external documentation 
+# can be added for each tagfile. The format of a tag file without 
+# this location is as follows: 
+#   TAGFILES = file1 file2 ... 
+# Adding location for the tag files is done as follows: 
+#   TAGFILES = file1=loc1 "file2 = loc2" ... 
+# where "loc1" and "loc2" can be relative or absolute paths or 
+# URLs. If a location is present for each tag, the installdox tool 
+# does not have to be run to correct the links.
+# Note that each tag file must have a unique name
+# (where the name does NOT include the path)
+# If a tag file is not located in the directory in which doxygen 
+# is run, you must also specify the path to the tagfile here.
+
+TAGFILES               = 
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create 
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE       = 
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed 
+# in the class index. If set to NO only the inherited external classes 
+# will be listed.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed 
+# in the modules index. If set to NO, only the current project's groups will 
+# be listed.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script 
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = @PERL@
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool   
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will 
+# generate a inheritance diagram (in HTML, RTF and LaTeX) for classes with base 
+# or super classes. Setting the tag to NO turns the diagrams off. Note that 
+# this option is superseded by the HAVE_DOT option below. This is only a 
+# fallback. It is recommended to install and use dot, since it yields more 
+# powerful graphs.
+
+CLASS_DIAGRAMS         = YES
+
+# If set to YES, the inheritance and collaboration graphs will hide 
+# inheritance and usage relations if the target is undocumented 
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is 
+# available from the path. This tool is part of Graphviz, a graph visualization 
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section 
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT               = NO
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect inheritance relations. Setting this tag to YES will force the 
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for each documented class showing the direct and 
+# indirect implementation dependencies (inheritance, containment, and 
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS and HAVE_DOT tags are set to YES then doxygen 
+# will generate a graph for groups, showing the direct groups dependencies
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES doxygen will generate inheritance and 
+# collaboration diagrams in a style similar to the OMG's Unified Modeling 
+# Language.
+
+UML_LOOK               = NO
+
+# If set to YES, the inheritance and collaboration graphs will show the 
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT 
+# tags are set to YES then doxygen will generate a graph for each documented 
+# file showing the direct and indirect include dependencies of the file with 
+# other documented files.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and 
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each 
+# documented header file showing the documented files that directly or 
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH and HAVE_DOT tags are set to YES then doxygen will 
+# generate a call dependency graph for every global function or class method. 
+# Note that enabling this option will significantly increase the time of a run. 
+# So in most cases it will be better to enable call graphs for selected 
+# functions only using the \callgraph command.
+
+CALL_GRAPH             = NO
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen 
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH, SHOW_DIRECTORIES and HAVE_DOT tags are set to YES 
+# then doxygen will show the dependencies a directory has on other directories 
+# in a graphical way. The dependency relations are determined by the #include
+# relations between the files in the directories.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images 
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT       = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be 
+# found. If left blank, it is assumed the dot tool can be found in the path.
+
+DOT_PATH               = 
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that 
+# contain dot files that are included in the documentation (see the 
+# \dotfile command).
+
+DOTFILE_DIRS           = 
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_WIDTH    = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT   = 1024
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the 
+# graphs generated by dot. A depth value of 3 means that only nodes reachable 
+# from the root by following a path via at most 3 edges will be shown. Nodes 
+# that lay further from the root node will be omitted. Note that setting this 
+# option to 1 or 2 may greatly reduce the computation time needed for large 
+# code bases. Also note that a graph may be further truncated if the graph's 
+# image dimensions are not sufficient to fit the graph (see MAX_DOT_GRAPH_WIDTH 
+# and MAX_DOT_GRAPH_HEIGHT). If 0 is used for the depth value (the default), 
+# the graph is not depth-constrained.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent 
+# background. This is disabled by default, which results in a white background. 
+# Warning: Depending on the platform used, enabling this option may lead to 
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to 
+# read).
+
+DOT_TRANSPARENT        = NO
+
+# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output 
+# files in one run (i.e. multiple -o and -T options on the command line). This 
+# makes dot run faster, but since only newer versions of dot (>1.8.10) 
+# support this, this feature is disabled by default.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will 
+# generate a legend page explaining the meaning of the various boxes and 
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will 
+# remove the intermediate dot files that are used to generate 
+# the various graphs.
+
+DOT_CLEANUP            = YES
+
+#---------------------------------------------------------------------------
+# Configuration::additions related to the search engine   
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be 
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE           = NO
Index: /tags/rel-0_0_1/ippdb/Makefile.am
===================================================================
--- /tags/rel-0_0_1/ippdb/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/Makefile.am	(revision 7462)
@@ -0,0 +1,51 @@
+SUBDIRS = src tests
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= ippdb.pc
+
+EXTRA_DIST = ippdb.pc.in Doxyfile.in
+
+if DOXYGEN
+
+man_MANS = \
+    $(top_builddir)/docs/man/man3/ippdb.3 \
+    $(top_builddir)/docs/man/man3/weatherRow.3 \
+    $(top_builddir)/docs/man/man3/skyp_transparencyRow.3 \
+    $(top_builddir)/docs/man/man3/skyp_absorptionRow.3 \
+    $(top_builddir)/docs/man/man3/skyp_emissionRow.3 \
+    $(top_builddir)/docs/man/man3/dimmRow.3 \
+    $(top_builddir)/docs/man/man3/skyp_irRow.3 \
+    $(top_builddir)/docs/man/man3/domeRow.3 \
+    $(top_builddir)/docs/man/man3/telescopeRow.3 \
+    $(top_builddir)/docs/man/man3/summitExpRow.3 \
+    $(top_builddir)/docs/man/man3/pzPendingExpRow.3 \
+    $(top_builddir)/docs/man/man3/pzPendingImfileRow.3 \
+    $(top_builddir)/docs/man/man3/newExpRow.3 \
+    $(top_builddir)/docs/man/man3/newImfileRow.3 \
+    $(top_builddir)/docs/man/man3/rawDetrendExpRow.3 \
+    $(top_builddir)/docs/man/man3/rawScienceExpRow.3 \
+    $(top_builddir)/docs/man/man3/rawImfileRow.3 \
+    $(top_builddir)/docs/man/man3/p1PendingExpRow.3 \
+    $(top_builddir)/docs/man/man3/p2PendingExpRow.3 \
+    $(top_builddir)/docs/man/man3/p2PendingImfileRow.3 \
+    $(top_builddir)/docs/man/man3/p2DoneExpRow.3 \
+    $(top_builddir)/docs/man/man3/p2DoneImfileRow.3 \
+    $(top_builddir)/docs/man/man3/p3PendingExpRow.3 \
+    $(top_builddir)/docs/man/man3/detRunRow.3 \
+    $(top_builddir)/docs/man/man3/detInputExpRow.3 \
+    $(top_builddir)/docs/man/man3/detProcessedImfileRow.3 \
+    $(top_builddir)/docs/man/man3/detStackedImfileRow.3 \
+    $(top_builddir)/docs/man/man3/detNormalizedImfileRow.3 \
+    $(top_builddir)/docs/man/man3/detMasterFrameRow.3 \
+    $(top_builddir)/docs/man/man3/detMasterImfileRow.3 \
+    $(top_builddir)/docs/man/man3/detResidImfileAnalysisRow.3 \
+    $(top_builddir)/docs/man/man3/detResidExpAnalysisRow.3 
+
+
+docs/man/man3/ippdb.3 docs/man/man3/weatherRow.3 docs/man/man3/skyp_transparencyRow.3 docs/man/man3/skyp_absorptionRow.3 docs/man/man3/skyp_emissionRow.3 docs/man/man3/dimmRow.3 docs/man/man3/skyp_irRow.3 docs/man/man3/domeRow.3 docs/man/man3/telescopeRow.3 docs/man/man3/summitExpRow.3 docs/man/man3/pzPendingExpRow.3 docs/man/man3/pzPendingImfileRow.3 docs/man/man3/newExpRow.3 docs/man/man3/newImfileRow.3 docs/man/man3/rawDetrendExpRow.3 docs/man/man3/rawScienceExpRow.3 docs/man/man3/rawImfileRow.3 docs/man/man3/p1PendingExpRow.3 docs/man/man3/p2PendingExpRow.3 docs/man/man3/p2PendingImfileRow.3 docs/man/man3/p2DoneExpRow.3 docs/man/man3/p2DoneImfileRow.3 docs/man/man3/p3PendingExpRow.3 docs/man/man3/detRunRow.3 docs/man/man3/detInputExpRow.3 docs/man/man3/detProcessedImfileRow.3 docs/man/man3/detStackedImfileRow.3 docs/man/man3/detNormalizedImfileRow.3 docs/man/man3/detMasterFrameRow.3 docs/man/man3/detMasterImfileRow.3 docs/man/man3/detResidImfileAnalysisRow.3 docs/man/man3/detResidExpAnalysisRow.3:
+	$(DOXYGEN)
+
+endif
+
+clean-local:
+	-rm -rf docs
Index: /tags/rel-0_0_1/ippdb/autogen.sh
===================================================================
--- /tags/rel-0_0_1/ippdb/autogen.sh	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/autogen.sh	(revision 7462)
@@ -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=ippdb
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL=aclocal
+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: /tags/rel-0_0_1/ippdb/configure.ac
===================================================================
--- /tags/rel-0_0_1/ippdb/configure.ac	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/configure.ac	(revision 7462)
@@ -0,0 +1,39 @@
+AC_PREREQ(2.59)
+
+AC_INIT([ippdb], [0.0.1], [pan-starrs.ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([ippdb.pc.in])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([config.h])
+AM_MAINTAINER_MODE
+
+AC_CONFIG_TESTDIR([tests])
+AC_CONFIG_FILES([tests/Makefile])
+AM_MISSING_PROG([AUTOM4TE], [autom4te])
+
+AC_LANG(C)
+AC_PROG_CC
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 0.9.0]) 
+
+AC_PATH_PROG([PERL], [perl], [missing])
+if test "$PERL" = "missing" ; then
+  AC_MSG_ERROR([perl is required])
+fi
+
+AC_PATH_PROG([doxygen], [doxygen], [missing])
+AM_CONDITIONAL([DOXYGEN], test "x$doxygen" = "xmissing")
+
+dnl is this the best was to setup recursive CFLAGS?
+ippdb_CFLAGS="-Wall -pedantic -std=c99 -fno-strict-aliasing"
+AC_SUBST([ippdb_CFLAGS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  ippdb.pc
+  Doxyfile
+])
+AC_OUTPUT
Index: /tags/rel-0_0_1/ippdb/ippdb.pc.in
===================================================================
--- /tags/rel-0_0_1/ippdb/ippdb.pc.in	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/ippdb.pc.in	(revision 7462)
@@ -0,0 +1,11 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: @PACKAGE@
+Description: some library
+Version: @VERSION@
+Requires: pslib
+Libs: -L${libdir} -l@PACKAGE@
+Cflags: -I${includedir}
Index: /tags/rel-0_0_1/ippdb/src/Makefile.am
===================================================================
--- /tags/rel-0_0_1/ippdb/src/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/src/Makefile.am	(revision 7462)
@@ -0,0 +1,10 @@
+AM_CFLAGS = @ippdb_CFLAGS@
+
+include_HEADERS = ippdb.h
+lib_LTLIBRARIES = libippdb.la
+libippdb_la_CFLAGS  = $(PSLIB_CFLAGS) $(AM_CFLAGS)
+libippdb_la_LIBS    = $(PSLIB_LIBS) $(AM_LIBS)
+libippdb_la_LDFLAGS = -release $(PACKAGE_VERSION)
+libippdb_la_SOURCES = \
+    ippdb.h \
+    ippdb.c
Index: /tags/rel-0_0_1/ippdb/src/ippdb.c
===================================================================
--- /tags/rel-0_0_1/ippdb/src/ippdb.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/src/ippdb.c	(revision 7462)
@@ -0,0 +1,14463 @@
+#include "ippdb.h"
+
+#define WEATHER_TABLE_NAME "weather"
+#define WEATHER_INDEX_NAME "position"
+#define SKYP_TRANSPARENCY_TABLE_NAME "skyp_transparency"
+#define SKYP_TRANSPARENCY_INDEX_NAME "position"
+#define SKYP_ABSORPTION_TABLE_NAME "skyp_absorption"
+#define SKYP_ABSORPTION_INDEX_NAME "position"
+#define SKYP_EMISSION_TABLE_NAME "skyp_emission"
+#define SKYP_EMISSION_INDEX_NAME "position"
+#define DIMM_TABLE_NAME "dimm"
+#define DIMM_INDEX_NAME "position"
+#define SKYP_IR_TABLE_NAME "skyp_ir"
+#define SKYP_IR_INDEX_NAME "position"
+#define DOME_TABLE_NAME "dome"
+#define DOME_INDEX_NAME "position"
+#define TELESCOPE_TABLE_NAME "telescope"
+#define TELESCOPE_INDEX_NAME "position"
+#define SUMMITEXP_TABLE_NAME "summitExp"
+#define SUMMITEXP_INDEX_NAME "position"
+#define PZPENDINGEXP_TABLE_NAME "pzPendingExp"
+#define PZPENDINGEXP_INDEX_NAME "position"
+#define PZPENDINGIMFILE_TABLE_NAME "pzPendingImfile"
+#define PZPENDINGIMFILE_INDEX_NAME "position"
+#define NEWEXP_TABLE_NAME "newExp"
+#define NEWEXP_INDEX_NAME "position"
+#define NEWIMFILE_TABLE_NAME "newImfile"
+#define NEWIMFILE_INDEX_NAME "position"
+#define RAWDETRENDEXP_TABLE_NAME "rawDetrendExp"
+#define RAWDETRENDEXP_INDEX_NAME "position"
+#define RAWSCIENCEEXP_TABLE_NAME "rawScienceExp"
+#define RAWSCIENCEEXP_INDEX_NAME "position"
+#define RAWIMFILE_TABLE_NAME "rawImfile"
+#define RAWIMFILE_INDEX_NAME "position"
+#define P1PENDINGEXP_TABLE_NAME "p1PendingExp"
+#define P1PENDINGEXP_INDEX_NAME "position"
+#define P2PENDINGEXP_TABLE_NAME "p2PendingExp"
+#define P2PENDINGEXP_INDEX_NAME "position"
+#define P2PENDINGIMFILE_TABLE_NAME "p2PendingImfile"
+#define P2PENDINGIMFILE_INDEX_NAME "position"
+#define P2DONEEXP_TABLE_NAME "p2DoneExp"
+#define P2DONEEXP_INDEX_NAME "position"
+#define P2DONEIMFILE_TABLE_NAME "p2DoneImfile"
+#define P2DONEIMFILE_INDEX_NAME "position"
+#define P3PENDINGEXP_TABLE_NAME "p3PendingExp"
+#define P3PENDINGEXP_INDEX_NAME "position"
+#define DETRUN_TABLE_NAME "detRun"
+#define DETRUN_INDEX_NAME "position"
+#define DETINPUTEXP_TABLE_NAME "detInputExp"
+#define DETINPUTEXP_INDEX_NAME "position"
+#define DETPROCESSEDIMFILE_TABLE_NAME "detProcessedImfile"
+#define DETPROCESSEDIMFILE_INDEX_NAME "position"
+#define DETSTACKEDIMFILE_TABLE_NAME "detStackedImfile"
+#define DETSTACKEDIMFILE_INDEX_NAME "position"
+#define DETNORMALIZEDIMFILE_TABLE_NAME "detNormalizedImfile"
+#define DETNORMALIZEDIMFILE_INDEX_NAME "position"
+#define DETMASTERFRAME_TABLE_NAME "detMasterFrame"
+#define DETMASTERFRAME_INDEX_NAME "position"
+#define DETMASTERIMFILE_TABLE_NAME "detMasterImfile"
+#define DETMASTERIMFILE_INDEX_NAME "position"
+#define DETRESIDIMFILEANALYSIS_TABLE_NAME "detResidImfileAnalysis"
+#define DETRESIDIMFILEANALYSIS_INDEX_NAME "position"
+#define DETRESIDEXPANALYSIS_TABLE_NAME "detResidExpAnalysis"
+#define DETRESIDEXPANALYSIS_INDEX_NAME "position"
+#define MAX_STRING_LENGTH 1024
+
+psDB *ippdbInit(const char *host, const char *user, const char *passwd, const char *dbname)
+{
+    return psDBInit(host, user, passwd, dbname);
+}
+
+void ippdbCleanup(psDB *dbh)
+{
+    psDBCleanup(dbh);
+}
+
+static void weatherRowFree(weatherRow *object);
+
+weatherRow *weatherRowAlloc(psF32 temp01, psF32 humi01, psF32 temp02, psF32 humi02, psF32 temp03, psF32 humi03, psF32 pressure)
+{
+    weatherRow      *object;
+
+    object = psAlloc(sizeof(weatherRow));
+    psMemSetDeallocator(object, (psFreeFunc)weatherRowFree);
+
+    object->temp01 = temp01;
+    object->humi01 = humi01;
+    object->temp02 = temp02;
+    object->humi02 = humi02;
+    object->temp03 = temp03;
+    object->humi03 = humi03;
+    object->pressure = pressure;
+
+    return object;
+}
+
+static void weatherRowFree(weatherRow *object)
+{
+}
+
+bool weatherCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, WEATHER_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", WEATHER_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, WEATHER_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool weatherDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, WEATHER_TABLE_NAME);
+}
+
+bool weatherInsert(psDB * dbh, psF32 temp01, psF32 humi01, psF32 temp02, psF32 humi02, psF32 temp03, psF32 humi03, psF32 pressure)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, temp01)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, humi01)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, temp02)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, humi02)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, temp03)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, humi03)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, pressure)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, WEATHER_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool weatherPop(psDB *dbh, psF32 *temp01, psF32 *humi01, psF32 *temp02, psF32 *humi02, psF32 *temp03, psF32 *humi03, psF32 *pressure)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, WEATHER_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", WEATHER_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, WEATHER_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", WEATHER_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, WEATHER_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, WEATHER_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *temp01 = psMetadataLookupF32(&status, row, "temp01");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp01");
+        psFree(row);
+        return false;
+    }
+    *humi01 = psMetadataLookupF32(&status, row, "humi01");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi01");
+        psFree(row);
+        return false;
+    }
+    *temp02 = psMetadataLookupF32(&status, row, "temp02");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp02");
+        psFree(row);
+        return false;
+    }
+    *humi02 = psMetadataLookupF32(&status, row, "humi02");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi02");
+        psFree(row);
+        return false;
+    }
+    *temp03 = psMetadataLookupF32(&status, row, "temp03");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp03");
+        psFree(row);
+        return false;
+    }
+    *humi03 = psMetadataLookupF32(&status, row, "humi03");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi03");
+        psFree(row);
+        return false;
+    }
+    *pressure = psMetadataLookupF32(&status, row, "pressure");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item pressure");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool weatherInsertObject(psDB *dbh, weatherRow *object)
+{
+    return weatherInsert(dbh, object->temp01, object->humi01, object->temp02, object->humi02, object->temp03, object->humi03, object->pressure);
+}
+
+weatherRow *weatherPopObject(psDB *dbh)
+{
+    psF32           temp01;
+    psF32           humi01;
+    psF32           temp02;
+    psF32           humi02;
+    psF32           temp03;
+    psF32           humi03;
+    psF32           pressure;
+
+    if (!weatherPop(dbh, &temp01, &humi01, &temp02, &humi02, &temp03, &humi03, &pressure)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return weatherRowAlloc(temp01, humi01, temp02, humi02, temp03, humi03, pressure);
+}
+
+bool weatherInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  WEATHER_TABLE_NAME
+    if (!psFitsMoveExtName(fits, WEATHER_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", WEATHER_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, WEATHER_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool weatherPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!weatherSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                WEATHER_TABLE_NAME, WEATHER_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool weatherSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, WEATHER_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, WEATHER_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", WEATHER_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, WEATHER_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *weatherMetadataFromObject(const weatherRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, object->temp01)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp01");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, object->humi01)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi01");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, object->temp02)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp02");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, object->humi02)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi02");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, object->temp03)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item temp03");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, object->humi03)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item humi03");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, object->pressure)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item pressure");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+weatherRow *weatherObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psF32           temp01;
+    psF32           humi01;
+    psF32           temp02;
+    psF32           humi02;
+    psF32           temp03;
+    psF32           humi03;
+    psF32           pressure;
+
+    temp01 = psMetadataLookupF32(&status, md, "temp01");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp01");
+        return false;
+    }
+    humi01 = psMetadataLookupF32(&status, md, "humi01");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi01");
+        return false;
+    }
+    temp02 = psMetadataLookupF32(&status, md, "temp02");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp02");
+        return false;
+    }
+    humi02 = psMetadataLookupF32(&status, md, "humi02");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi02");
+        return false;
+    }
+    temp03 = psMetadataLookupF32(&status, md, "temp03");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item temp03");
+        return false;
+    }
+    humi03 = psMetadataLookupF32(&status, md, "humi03");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item humi03");
+        return false;
+    }
+    pressure = psMetadataLookupF32(&status, md, "pressure");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item pressure");
+        return false;
+    }
+
+    return weatherRowAlloc(temp01, humi01, temp02, humi02, temp03, humi03, pressure);
+}
+psArray *weatherSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, WEATHER_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, WEATHER_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", WEATHER_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, weatherObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long weatherDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        weatherRow *object = objects->data[i];
+        psMetadata *where = weatherMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, WEATHER_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from weather");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void skyp_transparencyRowFree(skyp_transparencyRow *object);
+
+skyp_transparencyRow *skyp_transparencyRowAlloc(const char *filter, psF64 trans, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
+{
+    skyp_transparencyRow *object;
+
+    object = psAlloc(sizeof(skyp_transparencyRow));
+    psMemSetDeallocator(object, (psFreeFunc)skyp_transparencyRowFree);
+
+    object->filter = psStringCopy(filter);
+    object->trans = trans;
+    object->nstars = nstars;
+    object->ra = ra;
+    object->decl = decl;
+    object->exptime = exptime;
+    object->sky_bright = sky_bright;
+
+    return object;
+}
+
+static void skyp_transparencyRowFree(skyp_transparencyRow *object)
+{
+    psFree(object->filter);
+}
+
+bool skyp_transparencyCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_TRANSPARENCY_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_TRANSPARENCY_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, SKYP_TRANSPARENCY_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_transparencyDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, SKYP_TRANSPARENCY_TABLE_NAME);
+}
+
+bool skyp_transparencyInsert(psDB * dbh, const char *filter, psF64 trans, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, trans)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, nstars)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, SKYP_TRANSPARENCY_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_transparencyPop(psDB *dbh, char **filter, psF64 *trans, psS32 *nstars, psF64 *ra, psF64 *decl, psF32 *exptime, psF64 *sky_bright)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_TRANSPARENCY_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, SKYP_TRANSPARENCY_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_TRANSPARENCY_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_TRANSPARENCY_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *trans = psMetadataLookupF64(&status, row, "trans");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item trans");
+        psFree(row);
+        return false;
+    }
+    *nstars = psMetadataLookupS32(&status, row, "nstars");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
+        psFree(row);
+        return false;
+    }
+    *ra = psMetadataLookupF64(&status, row, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        psFree(row);
+        return false;
+    }
+    *decl = psMetadataLookupF64(&status, row, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        psFree(row);
+        return false;
+    }
+    *exptime = psMetadataLookupF32(&status, row, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        psFree(row);
+        return false;
+    }
+    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool skyp_transparencyInsertObject(psDB *dbh, skyp_transparencyRow *object)
+{
+    return skyp_transparencyInsert(dbh, object->filter, object->trans, object->nstars, object->ra, object->decl, object->exptime, object->sky_bright);
+}
+
+skyp_transparencyRow *skyp_transparencyPopObject(psDB *dbh)
+{
+    char            filter[256];
+    psF64           trans;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+
+    if (!skyp_transparencyPop(dbh, (char **)&filter, &trans, &nstars, &ra, &decl, &exptime, &sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return skyp_transparencyRowAlloc(filter, trans, nstars, ra, decl, exptime, sky_bright);
+}
+
+bool skyp_transparencyInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  SKYP_TRANSPARENCY_TABLE_NAME
+    if (!psFitsMoveExtName(fits, SKYP_TRANSPARENCY_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_TRANSPARENCY_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool skyp_transparencyPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!skyp_transparencySelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                SKYP_TRANSPARENCY_TABLE_NAME, SKYP_TRANSPARENCY_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool skyp_transparencySelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, SKYP_TRANSPARENCY_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_TRANSPARENCY_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_TRANSPARENCY_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *skyp_transparencyMetadataFromObject(const skyp_transparencyRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, object->trans)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item trans");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, object->nstars)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+skyp_transparencyRow *skyp_transparencyObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *filter;
+    psF64           trans;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    trans = psMetadataLookupF64(&status, md, "trans");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item trans");
+        return false;
+    }
+    nstars = psMetadataLookupS32(&status, md, "nstars");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
+        return false;
+    }
+    ra = psMetadataLookupF64(&status, md, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        return false;
+    }
+    decl = psMetadataLookupF64(&status, md, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        return false;
+    }
+    exptime = psMetadataLookupF32(&status, md, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        return false;
+    }
+    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        return false;
+    }
+
+    return skyp_transparencyRowAlloc(filter, trans, nstars, ra, decl, exptime, sky_bright);
+}
+psArray *skyp_transparencySelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, SKYP_TRANSPARENCY_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_TRANSPARENCY_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, skyp_transparencyObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long skyp_transparencyDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        skyp_transparencyRow *object = objects->data[i];
+        psMetadata *where = skyp_transparencyMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, SKYP_TRANSPARENCY_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_transparency");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void skyp_absorptionRowFree(skyp_absorptionRow *object);
+
+skyp_absorptionRow *skyp_absorptionRowAlloc(const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
+{
+    skyp_absorptionRow *object;
+
+    object = psAlloc(sizeof(skyp_absorptionRow));
+    psMemSetDeallocator(object, (psFreeFunc)skyp_absorptionRowFree);
+
+    object->disperser_id = psStringCopy(disperser_id);
+    object->atmcomp1 = atmcomp1;
+    object->atmcomp2 = atmcomp2;
+    object->atmcomp3 = atmcomp3;
+    object->nstars = nstars;
+    object->ra = ra;
+    object->decl = decl;
+    object->exptime = exptime;
+    object->sky_bright = sky_bright;
+
+    return object;
+}
+
+static void skyp_absorptionRowFree(skyp_absorptionRow *object)
+{
+    psFree(object->disperser_id);
+}
+
+bool skyp_absorptionCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_ABSORPTION_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_ABSORPTION_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, SKYP_ABSORPTION_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_absorptionDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, SKYP_ABSORPTION_TABLE_NAME);
+}
+
+bool skyp_absorptionInsert(psDB * dbh, const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psS32 nstars, psF64 ra, psF64 decl, psF32 exptime, psF64 sky_bright)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(disperser_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, atmcomp1)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, atmcomp2)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, atmcomp3)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, nstars)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, SKYP_ABSORPTION_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_absorptionPop(psDB *dbh, char **disperser_id, psF32 *atmcomp1, psF32 *atmcomp2, psF32 *atmcomp3, psS32 *nstars, psF64 *ra, psF64 *decl, psF32 *exptime, psF64 *sky_bright)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_ABSORPTION_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, SKYP_ABSORPTION_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_ABSORPTION_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_ABSORPTION_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, SKYP_ABSORPTION_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *disperser_id = psMetadataLookupPtr(&status, row, "disperser_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
+        psFree(row);
+        return false;
+    }
+    *atmcomp1 = psMetadataLookupF32(&status, row, "atmcomp1");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
+        psFree(row);
+        return false;
+    }
+    *atmcomp2 = psMetadataLookupF32(&status, row, "atmcomp2");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
+        psFree(row);
+        return false;
+    }
+    *atmcomp3 = psMetadataLookupF32(&status, row, "atmcomp3");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
+        psFree(row);
+        return false;
+    }
+    *nstars = psMetadataLookupS32(&status, row, "nstars");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
+        psFree(row);
+        return false;
+    }
+    *ra = psMetadataLookupF64(&status, row, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        psFree(row);
+        return false;
+    }
+    *decl = psMetadataLookupF64(&status, row, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        psFree(row);
+        return false;
+    }
+    *exptime = psMetadataLookupF32(&status, row, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        psFree(row);
+        return false;
+    }
+    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool skyp_absorptionInsertObject(psDB *dbh, skyp_absorptionRow *object)
+{
+    return skyp_absorptionInsert(dbh, object->disperser_id, object->atmcomp1, object->atmcomp2, object->atmcomp3, object->nstars, object->ra, object->decl, object->exptime, object->sky_bright);
+}
+
+skyp_absorptionRow *skyp_absorptionPopObject(psDB *dbh)
+{
+    char            disperser_id[256];
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+
+    if (!skyp_absorptionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &nstars, &ra, &decl, &exptime, &sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return skyp_absorptionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, nstars, ra, decl, exptime, sky_bright);
+}
+
+bool skyp_absorptionInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  SKYP_ABSORPTION_TABLE_NAME
+    if (!psFitsMoveExtName(fits, SKYP_ABSORPTION_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_ABSORPTION_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, SKYP_ABSORPTION_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool skyp_absorptionPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!skyp_absorptionSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                SKYP_ABSORPTION_TABLE_NAME, SKYP_ABSORPTION_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool skyp_absorptionSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, SKYP_ABSORPTION_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_ABSORPTION_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_ABSORPTION_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *skyp_absorptionMetadataFromObject(const skyp_absorptionRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(object->disperser_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, object->atmcomp1)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, object->atmcomp2)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, object->atmcomp3)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, object->nstars)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item nstars");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+skyp_absorptionRow *skyp_absorptionObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *disperser_id;
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+
+    disperser_id = psMetadataLookupPtr(&status, md, "disperser_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
+        return false;
+    }
+    atmcomp1 = psMetadataLookupF32(&status, md, "atmcomp1");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
+        return false;
+    }
+    atmcomp2 = psMetadataLookupF32(&status, md, "atmcomp2");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
+        return false;
+    }
+    atmcomp3 = psMetadataLookupF32(&status, md, "atmcomp3");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
+        return false;
+    }
+    nstars = psMetadataLookupS32(&status, md, "nstars");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item nstars");
+        return false;
+    }
+    ra = psMetadataLookupF64(&status, md, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        return false;
+    }
+    decl = psMetadataLookupF64(&status, md, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        return false;
+    }
+    exptime = psMetadataLookupF32(&status, md, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        return false;
+    }
+    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        return false;
+    }
+
+    return skyp_absorptionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, nstars, ra, decl, exptime, sky_bright);
+}
+psArray *skyp_absorptionSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_ABSORPTION_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, SKYP_ABSORPTION_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_ABSORPTION_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, skyp_absorptionObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long skyp_absorptionDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        skyp_absorptionRow *object = objects->data[i];
+        psMetadata *where = skyp_absorptionMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, SKYP_ABSORPTION_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_absorption");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void skyp_emissionRowFree(skyp_emissionRow *object);
+
+skyp_emissionRow *skyp_emissionRowAlloc(const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psF32 continuum, psF32 exptime)
+{
+    skyp_emissionRow *object;
+
+    object = psAlloc(sizeof(skyp_emissionRow));
+    psMemSetDeallocator(object, (psFreeFunc)skyp_emissionRowFree);
+
+    object->disperser_id = psStringCopy(disperser_id);
+    object->atmcomp1 = atmcomp1;
+    object->atmcomp2 = atmcomp2;
+    object->atmcomp3 = atmcomp3;
+    object->continuum = continuum;
+    object->exptime = exptime;
+
+    return object;
+}
+
+static void skyp_emissionRowFree(skyp_emissionRow *object)
+{
+    psFree(object->disperser_id);
+}
+
+bool skyp_emissionCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_EMISSION_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_EMISSION_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, SKYP_EMISSION_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_emissionDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, SKYP_EMISSION_TABLE_NAME);
+}
+
+bool skyp_emissionInsert(psDB * dbh, const char *disperser_id, psF32 atmcomp1, psF32 atmcomp2, psF32 atmcomp3, psF32 continuum, psF32 exptime)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(disperser_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, atmcomp1)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, atmcomp2)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, atmcomp3)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, continuum)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, SKYP_EMISSION_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_emissionPop(psDB *dbh, char **disperser_id, psF32 *atmcomp1, psF32 *atmcomp2, psF32 *atmcomp3, psF32 *continuum, psF32 *exptime)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_EMISSION_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, SKYP_EMISSION_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_EMISSION_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_EMISSION_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, SKYP_EMISSION_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *disperser_id = psMetadataLookupPtr(&status, row, "disperser_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
+        psFree(row);
+        return false;
+    }
+    *atmcomp1 = psMetadataLookupF32(&status, row, "atmcomp1");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
+        psFree(row);
+        return false;
+    }
+    *atmcomp2 = psMetadataLookupF32(&status, row, "atmcomp2");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
+        psFree(row);
+        return false;
+    }
+    *atmcomp3 = psMetadataLookupF32(&status, row, "atmcomp3");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
+        psFree(row);
+        return false;
+    }
+    *continuum = psMetadataLookupF32(&status, row, "continuum");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item continuum");
+        psFree(row);
+        return false;
+    }
+    *exptime = psMetadataLookupF32(&status, row, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool skyp_emissionInsertObject(psDB *dbh, skyp_emissionRow *object)
+{
+    return skyp_emissionInsert(dbh, object->disperser_id, object->atmcomp1, object->atmcomp2, object->atmcomp3, object->continuum, object->exptime);
+}
+
+skyp_emissionRow *skyp_emissionPopObject(psDB *dbh)
+{
+    char            disperser_id[256];
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psF32           continuum;
+    psF32           exptime;
+
+    if (!skyp_emissionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &continuum, &exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return skyp_emissionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, continuum, exptime);
+}
+
+bool skyp_emissionInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  SKYP_EMISSION_TABLE_NAME
+    if (!psFitsMoveExtName(fits, SKYP_EMISSION_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_EMISSION_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, SKYP_EMISSION_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool skyp_emissionPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!skyp_emissionSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                SKYP_EMISSION_TABLE_NAME, SKYP_EMISSION_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool skyp_emissionSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, SKYP_EMISSION_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_EMISSION_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_EMISSION_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *skyp_emissionMetadataFromObject(const skyp_emissionRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, psStringCopy(object->disperser_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item disperser_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, object->atmcomp1)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp1");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, object->atmcomp2)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp2");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, object->atmcomp3)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item atmcomp3");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, object->continuum)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item continuum");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, object->exptime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exptime");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+skyp_emissionRow *skyp_emissionObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *disperser_id;
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psF32           continuum;
+    psF32           exptime;
+
+    disperser_id = psMetadataLookupPtr(&status, md, "disperser_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item disperser_id");
+        return false;
+    }
+    atmcomp1 = psMetadataLookupF32(&status, md, "atmcomp1");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp1");
+        return false;
+    }
+    atmcomp2 = psMetadataLookupF32(&status, md, "atmcomp2");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp2");
+        return false;
+    }
+    atmcomp3 = psMetadataLookupF32(&status, md, "atmcomp3");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item atmcomp3");
+        return false;
+    }
+    continuum = psMetadataLookupF32(&status, md, "continuum");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item continuum");
+        return false;
+    }
+    exptime = psMetadataLookupF32(&status, md, "exptime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exptime");
+        return false;
+    }
+
+    return skyp_emissionRowAlloc(disperser_id, atmcomp1, atmcomp2, atmcomp3, continuum, exptime);
+}
+psArray *skyp_emissionSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_EMISSION_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, SKYP_EMISSION_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_EMISSION_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, skyp_emissionObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long skyp_emissionDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        skyp_emissionRow *object = objects->data[i];
+        psMetadata *where = skyp_emissionMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, SKYP_EMISSION_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_emission");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void dimmRowFree(dimmRow *object);
+
+dimmRow *dimmRowAlloc(psF32 sigmax, psF32 sigmay, psF32 fwhm, psF64 ra, psF64 decl, psF32 expttime, const char *telescope_id)
+{
+    dimmRow         *object;
+
+    object = psAlloc(sizeof(dimmRow));
+    psMemSetDeallocator(object, (psFreeFunc)dimmRowFree);
+
+    object->sigmax = sigmax;
+    object->sigmay = sigmay;
+    object->fwhm = fwhm;
+    object->ra = ra;
+    object->decl = decl;
+    object->expttime = expttime;
+    object->telescope_id = psStringCopy(telescope_id);
+
+    return object;
+}
+
+static void dimmRowFree(dimmRow *object)
+{
+    psFree(object->telescope_id);
+}
+
+bool dimmCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DIMM_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DIMM_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DIMM_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool dimmDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DIMM_TABLE_NAME);
+}
+
+bool dimmInsert(psDB * dbh, psF32 sigmax, psF32 sigmay, psF32 fwhm, psF64 ra, psF64 decl, psF32 expttime, const char *telescope_id)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, sigmax)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, sigmay)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, fwhm)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, expttime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, psStringCopy(telescope_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DIMM_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool dimmPop(psDB *dbh, psF32 *sigmax, psF32 *sigmay, psF32 *fwhm, psF64 *ra, psF64 *decl, psF32 *expttime, char **telescope_id)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DIMM_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DIMM_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DIMM_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DIMM_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DIMM_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DIMM_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *sigmax = psMetadataLookupF32(&status, row, "sigmax");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmax");
+        psFree(row);
+        return false;
+    }
+    *sigmay = psMetadataLookupF32(&status, row, "sigmay");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmay");
+        psFree(row);
+        return false;
+    }
+    *fwhm = psMetadataLookupF32(&status, row, "fwhm");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fwhm");
+        psFree(row);
+        return false;
+    }
+    *ra = psMetadataLookupF64(&status, row, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        psFree(row);
+        return false;
+    }
+    *decl = psMetadataLookupF64(&status, row, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        psFree(row);
+        return false;
+    }
+    *expttime = psMetadataLookupF32(&status, row, "expttime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item expttime");
+        psFree(row);
+        return false;
+    }
+    *telescope_id = psMetadataLookupPtr(&status, row, "telescope_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope_id");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool dimmInsertObject(psDB *dbh, dimmRow *object)
+{
+    return dimmInsert(dbh, object->sigmax, object->sigmay, object->fwhm, object->ra, object->decl, object->expttime, object->telescope_id);
+}
+
+dimmRow *dimmPopObject(psDB *dbh)
+{
+    psF32           sigmax;
+    psF32           sigmay;
+    psF32           fwhm;
+    psF64           ra;
+    psF64           decl;
+    psF32           expttime;
+    char            telescope_id[256];
+
+    if (!dimmPop(dbh, &sigmax, &sigmay, &fwhm, &ra, &decl, &expttime, (char **)&telescope_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return dimmRowAlloc(sigmax, sigmay, fwhm, ra, decl, expttime, telescope_id);
+}
+
+bool dimmInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DIMM_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DIMM_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DIMM_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DIMM_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool dimmPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!dimmSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DIMM_TABLE_NAME, DIMM_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool dimmSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DIMM_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DIMM_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DIMM_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DIMM_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *dimmMetadataFromObject(const dimmRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, object->sigmax)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmax");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, object->sigmay)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sigmay");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, object->fwhm)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fwhm");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, object->expttime)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item expttime");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, psStringCopy(object->telescope_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope_id");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+dimmRow *dimmObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psF32           sigmax;
+    psF32           sigmay;
+    psF32           fwhm;
+    psF64           ra;
+    psF64           decl;
+    psF32           expttime;
+    char            *telescope_id;
+
+    sigmax = psMetadataLookupF32(&status, md, "sigmax");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmax");
+        return false;
+    }
+    sigmay = psMetadataLookupF32(&status, md, "sigmay");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sigmay");
+        return false;
+    }
+    fwhm = psMetadataLookupF32(&status, md, "fwhm");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fwhm");
+        return false;
+    }
+    ra = psMetadataLookupF64(&status, md, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        return false;
+    }
+    decl = psMetadataLookupF64(&status, md, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        return false;
+    }
+    expttime = psMetadataLookupF32(&status, md, "expttime");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item expttime");
+        return false;
+    }
+    telescope_id = psMetadataLookupPtr(&status, md, "telescope_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope_id");
+        return false;
+    }
+
+    return dimmRowAlloc(sigmax, sigmay, fwhm, ra, decl, expttime, telescope_id);
+}
+psArray *dimmSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DIMM_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DIMM_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DIMM_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, dimmObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long dimmDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        dimmRow *object = objects->data[i];
+        psMetadata *where = dimmMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DIMM_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from dimm");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void skyp_irRowFree(skyp_irRow *object);
+
+skyp_irRow *skyp_irRowAlloc(psF64 sky_bright, psF64 sky_var, psF64 ra, psF64 decl, psF32 fov_x, psF32 fov_y)
+{
+    skyp_irRow      *object;
+
+    object = psAlloc(sizeof(skyp_irRow));
+    psMemSetDeallocator(object, (psFreeFunc)skyp_irRowFree);
+
+    object->sky_bright = sky_bright;
+    object->sky_var = sky_var;
+    object->ra = ra;
+    object->decl = decl;
+    object->fov_x = fov_x;
+    object->fov_y = fov_y;
+
+    return object;
+}
+
+static void skyp_irRowFree(skyp_irRow *object)
+{
+}
+
+bool skyp_irCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, SKYP_IR_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SKYP_IR_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, SKYP_IR_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_irDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, SKYP_IR_TABLE_NAME);
+}
+
+bool skyp_irInsert(psDB * dbh, psF64 sky_bright, psF64 sky_var, psF64 ra, psF64 decl, psF32 fov_x, psF32 fov_y)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, sky_var)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, fov_x)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, fov_y)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, SKYP_IR_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool skyp_irPop(psDB *dbh, psF64 *sky_bright, psF64 *sky_var, psF64 *ra, psF64 *decl, psF32 *fov_x, psF32 *fov_y)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, SKYP_IR_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_IR_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, SKYP_IR_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SKYP_IR_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, SKYP_IR_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, SKYP_IR_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *sky_bright = psMetadataLookupF64(&status, row, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        psFree(row);
+        return false;
+    }
+    *sky_var = psMetadataLookupF64(&status, row, "sky_var");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_var");
+        psFree(row);
+        return false;
+    }
+    *ra = psMetadataLookupF64(&status, row, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        psFree(row);
+        return false;
+    }
+    *decl = psMetadataLookupF64(&status, row, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        psFree(row);
+        return false;
+    }
+    *fov_x = psMetadataLookupF32(&status, row, "fov_x");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_x");
+        psFree(row);
+        return false;
+    }
+    *fov_y = psMetadataLookupF32(&status, row, "fov_y");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_y");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool skyp_irInsertObject(psDB *dbh, skyp_irRow *object)
+{
+    return skyp_irInsert(dbh, object->sky_bright, object->sky_var, object->ra, object->decl, object->fov_x, object->fov_y);
+}
+
+skyp_irRow *skyp_irPopObject(psDB *dbh)
+{
+    psF64           sky_bright;
+    psF64           sky_var;
+    psF64           ra;
+    psF64           decl;
+    psF32           fov_x;
+    psF32           fov_y;
+
+    if (!skyp_irPop(dbh, &sky_bright, &sky_var, &ra, &decl, &fov_x, &fov_y)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return skyp_irRowAlloc(sky_bright, sky_var, ra, decl, fov_x, fov_y);
+}
+
+bool skyp_irInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  SKYP_IR_TABLE_NAME
+    if (!psFitsMoveExtName(fits, SKYP_IR_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SKYP_IR_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, SKYP_IR_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool skyp_irPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!skyp_irSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                SKYP_IR_TABLE_NAME, SKYP_IR_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool skyp_irSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_IR_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, SKYP_IR_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_IR_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, SKYP_IR_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *skyp_irMetadataFromObject(const skyp_irRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, object->sky_bright)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_bright");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, object->sky_var)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item sky_var");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, object->fov_x)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_x");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, object->fov_y)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item fov_y");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+skyp_irRow *skyp_irObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psF64           sky_bright;
+    psF64           sky_var;
+    psF64           ra;
+    psF64           decl;
+    psF32           fov_x;
+    psF32           fov_y;
+
+    sky_bright = psMetadataLookupF64(&status, md, "sky_bright");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_bright");
+        return false;
+    }
+    sky_var = psMetadataLookupF64(&status, md, "sky_var");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item sky_var");
+        return false;
+    }
+    ra = psMetadataLookupF64(&status, md, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        return false;
+    }
+    decl = psMetadataLookupF64(&status, md, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        return false;
+    }
+    fov_x = psMetadataLookupF32(&status, md, "fov_x");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_x");
+        return false;
+    }
+    fov_y = psMetadataLookupF32(&status, md, "fov_y");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item fov_y");
+        return false;
+    }
+
+    return skyp_irRowAlloc(sky_bright, sky_var, ra, decl, fov_x, fov_y);
+}
+psArray *skyp_irSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SKYP_IR_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, SKYP_IR_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SKYP_IR_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, skyp_irObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long skyp_irDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        skyp_irRow *object = objects->data[i];
+        psMetadata *where = skyp_irMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, SKYP_IR_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from skyp_ir");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void domeRowFree(domeRow *object);
+
+domeRow *domeRowAlloc(psF32 az, bool open, bool light, bool track)
+{
+    domeRow         *object;
+
+    object = psAlloc(sizeof(domeRow));
+    psMemSetDeallocator(object, (psFreeFunc)domeRowFree);
+
+    object->az = az;
+    object->open = open;
+    object->light = light;
+    object->track = track;
+
+    return object;
+}
+
+static void domeRowFree(domeRow *object)
+{
+}
+
+bool domeCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DOME_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DOME_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, true)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, true)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, true)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DOME_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool domeDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DOME_TABLE_NAME);
+}
+
+bool domeInsert(psDB * dbh, psF32 az, bool open, bool light, bool track)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, az)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, open)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, light)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, track)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DOME_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool domePop(psDB *dbh, psF32 *az, bool *open, bool *light, bool *track)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DOME_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DOME_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DOME_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DOME_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DOME_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DOME_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *az = psMetadataLookupF32(&status, row, "az");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
+        psFree(row);
+        return false;
+    }
+    *open = psMetadataLookupBool(&status, row, "open");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item open");
+        psFree(row);
+        return false;
+    }
+    *light = psMetadataLookupBool(&status, row, "light");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item light");
+        psFree(row);
+        return false;
+    }
+    *track = psMetadataLookupBool(&status, row, "track");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item track");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool domeInsertObject(psDB *dbh, domeRow *object)
+{
+    return domeInsert(dbh, object->az, object->open, object->light, object->track);
+}
+
+domeRow *domePopObject(psDB *dbh)
+{
+    psF32           az;
+    bool            open;
+    bool            light;
+    bool            track;
+
+    if (!domePop(dbh, &az, &open, &light, &track)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return domeRowAlloc(az, open, light, track);
+}
+
+bool domeInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DOME_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DOME_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DOME_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DOME_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool domePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!domeSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DOME_TABLE_NAME, DOME_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool domeSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DOME_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DOME_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DOME_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DOME_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *domeMetadataFromObject(const domeRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, object->az)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, object->open)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item open");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, object->light)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item light");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, object->track)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item track");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+domeRow *domeObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psF32           az;
+    bool            open;
+    bool            light;
+    bool            track;
+
+    az = psMetadataLookupF32(&status, md, "az");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
+        return false;
+    }
+    open = psMetadataLookupBool(&status, md, "open");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item open");
+        return false;
+    }
+    light = psMetadataLookupBool(&status, md, "light");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item light");
+        return false;
+    }
+    track = psMetadataLookupBool(&status, md, "track");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item track");
+        return false;
+    }
+
+    return domeRowAlloc(az, open, light, track);
+}
+psArray *domeSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DOME_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DOME_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DOME_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, domeObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long domeDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        domeRow *object = objects->data[i];
+        psMetadata *where = domeMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DOME_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from dome");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void telescopeRowFree(telescopeRow *object);
+
+telescopeRow *telescopeRowAlloc(const char *guide, psF32 alt, psF32 az, psF64 ra, psF64 decl)
+{
+    telescopeRow    *object;
+
+    object = psAlloc(sizeof(telescopeRow));
+    psMemSetDeallocator(object, (psFreeFunc)telescopeRowFree);
+
+    object->guide = psStringCopy(guide);
+    object->alt = alt;
+    object->az = az;
+    object->ra = ra;
+    object->decl = decl;
+
+    return object;
+}
+
+static void telescopeRowFree(telescopeRow *object)
+{
+    psFree(object->guide);
+}
+
+bool telescopeCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, TELESCOPE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", TELESCOPE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, TELESCOPE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool telescopeDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, TELESCOPE_TABLE_NAME);
+}
+
+bool telescopeInsert(psDB * dbh, const char *guide, psF32 alt, psF32 az, psF64 ra, psF64 decl)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, psStringCopy(guide))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, alt)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, az)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, TELESCOPE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool telescopePop(psDB *dbh, char **guide, psF32 *alt, psF32 *az, psF64 *ra, psF64 *decl)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, TELESCOPE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", TELESCOPE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, TELESCOPE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", TELESCOPE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, TELESCOPE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, TELESCOPE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *guide = psMetadataLookupPtr(&status, row, "guide");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item guide");
+        psFree(row);
+        return false;
+    }
+    *alt = psMetadataLookupF32(&status, row, "alt");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item alt");
+        psFree(row);
+        return false;
+    }
+    *az = psMetadataLookupF32(&status, row, "az");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
+        psFree(row);
+        return false;
+    }
+    *ra = psMetadataLookupF64(&status, row, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        psFree(row);
+        return false;
+    }
+    *decl = psMetadataLookupF64(&status, row, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool telescopeInsertObject(psDB *dbh, telescopeRow *object)
+{
+    return telescopeInsert(dbh, object->guide, object->alt, object->az, object->ra, object->decl);
+}
+
+telescopeRow *telescopePopObject(psDB *dbh)
+{
+    char            guide[256];
+    psF32           alt;
+    psF32           az;
+    psF64           ra;
+    psF64           decl;
+
+    if (!telescopePop(dbh, (char **)&guide, &alt, &az, &ra, &decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return telescopeRowAlloc(guide, alt, az, ra, decl);
+}
+
+bool telescopeInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  TELESCOPE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, TELESCOPE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", TELESCOPE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, TELESCOPE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool telescopePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!telescopeSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                TELESCOPE_TABLE_NAME, TELESCOPE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool telescopeSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, TELESCOPE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, TELESCOPE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", TELESCOPE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, TELESCOPE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *telescopeMetadataFromObject(const telescopeRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, psStringCopy(object->guide))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item guide");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, object->alt)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item alt");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, object->az)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item az");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, object->ra)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item ra");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, object->decl)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item decl");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+telescopeRow *telescopeObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *guide;
+    psF32           alt;
+    psF32           az;
+    psF64           ra;
+    psF64           decl;
+
+    guide = psMetadataLookupPtr(&status, md, "guide");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item guide");
+        return false;
+    }
+    alt = psMetadataLookupF32(&status, md, "alt");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item alt");
+        return false;
+    }
+    az = psMetadataLookupF32(&status, md, "az");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item az");
+        return false;
+    }
+    ra = psMetadataLookupF64(&status, md, "ra");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item ra");
+        return false;
+    }
+    decl = psMetadataLookupF64(&status, md, "decl");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item decl");
+        return false;
+    }
+
+    return telescopeRowAlloc(guide, alt, az, ra, decl);
+}
+psArray *telescopeSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, TELESCOPE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, TELESCOPE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", TELESCOPE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, telescopeObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long telescopeDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        telescopeRow *object = objects->data[i];
+        psMetadata *where = telescopeMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, TELESCOPE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from telescope");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void summitExpRowFree(summitExpRow *object);
+
+summitExpRow *summitExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, const char *uri)
+{
+    summitExpRow    *object;
+
+    object = psAlloc(sizeof(summitExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)summitExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->uri = psStringCopy(uri);
+
+    return object;
+}
+
+static void summitExpRowFree(summitExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->uri);
+}
+
+bool summitExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, SUMMITEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", SUMMITEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, SUMMITEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool summitExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, SUMMITEXP_TABLE_NAME);
+}
+
+bool summitExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, const char *uri)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, SUMMITEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool summitExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, char **uri)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, SUMMITEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SUMMITEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, SUMMITEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", SUMMITEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, SUMMITEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, SUMMITEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool summitExpInsertObject(psDB *dbh, summitExpRow *object)
+{
+    return summitExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->uri);
+}
+
+summitExpRow *summitExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    char            uri[256];
+
+    if (!summitExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, (char **)&uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return summitExpRowAlloc(exp_id, camera, telescope, exp_type, uri);
+}
+
+bool summitExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  SUMMITEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, SUMMITEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", SUMMITEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, SUMMITEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool summitExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!summitExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                SUMMITEXP_TABLE_NAME, SUMMITEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool summitExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SUMMITEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, SUMMITEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SUMMITEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, SUMMITEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *summitExpMetadataFromObject(const summitExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+summitExpRow *summitExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    char            *uri;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+
+    return summitExpRowAlloc(exp_id, camera, telescope, exp_type, uri);
+}
+psArray *summitExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, SUMMITEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, SUMMITEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", SUMMITEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, summitExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long summitExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        summitExpRow *object = objects->data[i];
+        psMetadata *where = summitExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, SUMMITEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from summitExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void pzPendingExpRowFree(pzPendingExpRow *object);
+
+pzPendingExpRow *pzPendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
+{
+    pzPendingExpRow *object;
+
+    object = psAlloc(sizeof(pzPendingExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)pzPendingExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+
+    return object;
+}
+
+static void pzPendingExpRowFree(pzPendingExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+}
+
+bool pzPendingExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, PZPENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", PZPENDINGEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, PZPENDINGEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool pzPendingExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, PZPENDINGEXP_TABLE_NAME);
+}
+
+bool pzPendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, PZPENDINGEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool pzPendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, PZPENDINGEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, PZPENDINGEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, PZPENDINGEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool pzPendingExpInsertObject(psDB *dbh, pzPendingExpRow *object)
+{
+    return pzPendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles);
+}
+
+pzPendingExpRow *pzPendingExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+
+    if (!pzPendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return pzPendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
+}
+
+bool pzPendingExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  PZPENDINGEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, PZPENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", PZPENDINGEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, PZPENDINGEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool pzPendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!pzPendingExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                PZPENDINGEXP_TABLE_NAME, PZPENDINGEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool pzPendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, PZPENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, PZPENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *pzPendingExpMetadataFromObject(const pzPendingExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+pzPendingExpRow *pzPendingExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+
+    return pzPendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
+}
+psArray *pzPendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, PZPENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, pzPendingExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long pzPendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        pzPendingExpRow *object = objects->data[i];
+        psMetadata *where = pzPendingExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, PZPENDINGEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from pzPendingExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void pzPendingImfileRowFree(pzPendingImfileRow *object);
+
+pzPendingImfileRow *pzPendingImfileRowAlloc(const char *exp_id, psS32 bytes, const char *md5sum, const char *class, const char *class_id, const char *uri)
+{
+    pzPendingImfileRow *object;
+
+    object = psAlloc(sizeof(pzPendingImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)pzPendingImfileRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->bytes = bytes;
+    object->md5sum = psStringCopy(md5sum);
+    object->class = psStringCopy(class);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+
+    return object;
+}
+
+static void pzPendingImfileRowFree(pzPendingImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->md5sum);
+    psFree(object->class);
+    psFree(object->class_id);
+    psFree(object->uri);
+}
+
+bool pzPendingImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, PZPENDINGIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", PZPENDINGIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, "32")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, PZPENDINGIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool pzPendingImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, PZPENDINGIMFILE_TABLE_NAME);
+}
+
+bool pzPendingImfileInsert(psDB * dbh, const char *exp_id, psS32 bytes, const char *md5sum, const char *class, const char *class_id, const char *uri)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, bytes)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, psStringCopy(md5sum))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(class))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, PZPENDINGIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool pzPendingImfilePop(psDB *dbh, char **exp_id, psS32 *bytes, char **md5sum, char **class, char **class_id, char **uri)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, PZPENDINGIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", PZPENDINGIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, PZPENDINGIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, PZPENDINGIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *bytes = psMetadataLookupS32(&status, row, "bytes");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bytes");
+        psFree(row);
+        return false;
+    }
+    *md5sum = psMetadataLookupPtr(&status, row, "md5sum");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item md5sum");
+        psFree(row);
+        return false;
+    }
+    *class = psMetadataLookupPtr(&status, row, "class");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool pzPendingImfileInsertObject(psDB *dbh, pzPendingImfileRow *object)
+{
+    return pzPendingImfileInsert(dbh, object->exp_id, object->bytes, object->md5sum, object->class, object->class_id, object->uri);
+}
+
+pzPendingImfileRow *pzPendingImfilePopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    psS32           bytes;
+    char            md5sum[256];
+    char            class[256];
+    char            class_id[256];
+    char            uri[256];
+
+    if (!pzPendingImfilePop(dbh, (char **)&exp_id, &bytes, (char **)&md5sum, (char **)&class, (char **)&class_id, (char **)&uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return pzPendingImfileRowAlloc(exp_id, bytes, md5sum, class, class_id, uri);
+}
+
+bool pzPendingImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  PZPENDINGIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, PZPENDINGIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", PZPENDINGIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, PZPENDINGIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool pzPendingImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!pzPendingImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                PZPENDINGIMFILE_TABLE_NAME, PZPENDINGIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool pzPendingImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, PZPENDINGIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, PZPENDINGIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *pzPendingImfileMetadataFromObject(const pzPendingImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, object->bytes)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item bytes");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, psStringCopy(object->md5sum))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item md5sum");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(object->class))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+pzPendingImfileRow *pzPendingImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    psS32           bytes;
+    char            *md5sum;
+    char            *class;
+    char            *class_id;
+    char            *uri;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    bytes = psMetadataLookupS32(&status, md, "bytes");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item bytes");
+        return false;
+    }
+    md5sum = psMetadataLookupPtr(&status, md, "md5sum");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item md5sum");
+        return false;
+    }
+    class = psMetadataLookupPtr(&status, md, "class");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+
+    return pzPendingImfileRowAlloc(exp_id, bytes, md5sum, class, class_id, uri);
+}
+psArray *pzPendingImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, PZPENDINGIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, PZPENDINGIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", PZPENDINGIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, pzPendingImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long pzPendingImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        pzPendingImfileRow *object = objects->data[i];
+        psMetadata *where = pzPendingImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, PZPENDINGIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from pzPendingImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void newExpRowFree(newExpRow *object);
+
+newExpRow *newExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
+{
+    newExpRow       *object;
+
+    object = psAlloc(sizeof(newExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)newExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+
+    return object;
+}
+
+static void newExpRowFree(newExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+}
+
+bool newExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, NEWEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", NEWEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, NEWEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool newExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, NEWEXP_TABLE_NAME);
+}
+
+bool newExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, NEWEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool newExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, NEWEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, NEWEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, NEWEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, NEWEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool newExpInsertObject(psDB *dbh, newExpRow *object)
+{
+    return newExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles);
+}
+
+newExpRow *newExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+
+    if (!newExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return newExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
+}
+
+bool newExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  NEWEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, NEWEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", NEWEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, NEWEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool newExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!newExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                NEWEXP_TABLE_NAME, NEWEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool newExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, NEWEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, NEWEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, NEWEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *newExpMetadataFromObject(const newExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+newExpRow *newExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+
+    return newExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles);
+}
+psArray *newExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, NEWEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, NEWEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, newExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long newExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        newExpRow *object = objects->data[i];
+        psMetadata *where = newExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, NEWEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from newExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void newImfileRowFree(newImfileRow *object);
+
+newImfileRow *newImfileRowAlloc(const char *exp_id, const char *class, const char *class_id, const char *uri)
+{
+    newImfileRow    *object;
+
+    object = psAlloc(sizeof(newImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)newImfileRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->class = psStringCopy(class);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+
+    return object;
+}
+
+static void newImfileRowFree(newImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class);
+    psFree(object->class_id);
+    psFree(object->uri);
+}
+
+bool newImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, NEWIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", NEWIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, NEWIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool newImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, NEWIMFILE_TABLE_NAME);
+}
+
+bool newImfileInsert(psDB * dbh, const char *exp_id, const char *class, const char *class_id, const char *uri)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(class))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, NEWIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool newImfilePop(psDB *dbh, char **exp_id, char **class, char **class_id, char **uri)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, NEWIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, NEWIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", NEWIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, NEWIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, NEWIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class = psMetadataLookupPtr(&status, row, "class");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool newImfileInsertObject(psDB *dbh, newImfileRow *object)
+{
+    return newImfileInsert(dbh, object->exp_id, object->class, object->class_id, object->uri);
+}
+
+newImfileRow *newImfilePopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            class[256];
+    char            class_id[256];
+    char            uri[256];
+
+    if (!newImfilePop(dbh, (char **)&exp_id, (char **)&class, (char **)&class_id, (char **)&uri)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return newImfileRowAlloc(exp_id, class, class_id, uri);
+}
+
+bool newImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  NEWIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, NEWIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", NEWIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, NEWIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool newImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!newImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                NEWIMFILE_TABLE_NAME, NEWIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool newImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, NEWIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, NEWIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, NEWIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *newImfileMetadataFromObject(const newImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, psStringCopy(object->class))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+newImfileRow *newImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *class;
+    char            *class_id;
+    char            *uri;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class = psMetadataLookupPtr(&status, md, "class");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+
+    return newImfileRowAlloc(exp_id, class, class_id, uri);
+}
+psArray *newImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, NEWIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, NEWIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", NEWIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, newImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long newImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        newImfileRow *object = objects->data[i];
+        psMetadata *where = newImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, NEWIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from newImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void rawDetrendExpRowFree(rawDetrendExpRow *object);
+
+rawDetrendExpRow *rawDetrendExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    rawDetrendExpRow *object;
+
+    object = psAlloc(sizeof(rawDetrendExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)rawDetrendExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+
+    return object;
+}
+
+static void rawDetrendExpRowFree(rawDetrendExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+}
+
+bool rawDetrendExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWDETRENDEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWDETRENDEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, RAWDETRENDEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool rawDetrendExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, RAWDETRENDEXP_TABLE_NAME);
+}
+
+bool rawDetrendExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, RAWDETRENDEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool rawDetrendExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWDETRENDEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, RAWDETRENDEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWDETRENDEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, RAWDETRENDEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, RAWDETRENDEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool rawDetrendExpInsertObject(psDB *dbh, rawDetrendExpRow *object)
+{
+    return rawDetrendExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
+}
+
+rawDetrendExpRow *rawDetrendExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+
+    if (!rawDetrendExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return rawDetrendExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+
+bool rawDetrendExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  RAWDETRENDEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, RAWDETRENDEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWDETRENDEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, RAWDETRENDEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool rawDetrendExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!rawDetrendExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                RAWDETRENDEXP_TABLE_NAME, RAWDETRENDEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool rawDetrendExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, RAWDETRENDEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWDETRENDEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, RAWDETRENDEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *rawDetrendExpMetadataFromObject(const rawDetrendExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+rawDetrendExpRow *rawDetrendExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+
+    return rawDetrendExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+psArray *rawDetrendExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWDETRENDEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, RAWDETRENDEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWDETRENDEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, rawDetrendExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long rawDetrendExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        rawDetrendExpRow *object = objects->data[i];
+        psMetadata *where = rawDetrendExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, RAWDETRENDEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawDetrendExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void rawScienceExpRowFree(rawScienceExpRow *object);
+
+rawScienceExpRow *rawScienceExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    rawScienceExpRow *object;
+
+    object = psAlloc(sizeof(rawScienceExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)rawScienceExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+
+    return object;
+}
+
+static void rawScienceExpRowFree(rawScienceExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+}
+
+bool rawScienceExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWSCIENCEEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWSCIENCEEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, RAWSCIENCEEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool rawScienceExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, RAWSCIENCEEXP_TABLE_NAME);
+}
+
+bool rawScienceExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, RAWSCIENCEEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool rawScienceExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWSCIENCEEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, RAWSCIENCEEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWSCIENCEEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, RAWSCIENCEEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, RAWSCIENCEEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool rawScienceExpInsertObject(psDB *dbh, rawScienceExpRow *object)
+{
+    return rawScienceExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
+}
+
+rawScienceExpRow *rawScienceExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+
+    if (!rawScienceExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return rawScienceExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+
+bool rawScienceExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  RAWSCIENCEEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, RAWSCIENCEEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWSCIENCEEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, RAWSCIENCEEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool rawScienceExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!rawScienceExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                RAWSCIENCEEXP_TABLE_NAME, RAWSCIENCEEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool rawScienceExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, RAWSCIENCEEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWSCIENCEEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, RAWSCIENCEEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *rawScienceExpMetadataFromObject(const rawScienceExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+rawScienceExpRow *rawScienceExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+
+    return rawScienceExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+psArray *rawScienceExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWSCIENCEEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, RAWSCIENCEEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWSCIENCEEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, rawScienceExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long rawScienceExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        rawScienceExpRow *object = objects->data[i];
+        psMetadata *where = rawScienceExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, RAWSCIENCEEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawScienceExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void rawImfileRowFree(rawImfileRow *object);
+
+rawImfileRow *rawImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats)
+{
+    rawImfileRow    *object;
+
+    object = psAlloc(sizeof(rawImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)rawImfileRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+
+    return object;
+}
+
+static void rawImfileRowFree(rawImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+}
+
+bool rawImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, RAWIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", RAWIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, RAWIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool rawImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, RAWIMFILE_TABLE_NAME);
+}
+
+bool rawImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, RAWIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool rawImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, RAWIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, RAWIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", RAWIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, RAWIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, RAWIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool rawImfileInsertObject(psDB *dbh, rawImfileRow *object)
+{
+    return rawImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats);
+}
+
+rawImfileRow *rawImfilePopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+
+    if (!rawImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return rawImfileRowAlloc(exp_id, class_id, uri, stats);
+}
+
+bool rawImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  RAWIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, RAWIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", RAWIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, RAWIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool rawImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!rawImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                RAWIMFILE_TABLE_NAME, RAWIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool rawImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, RAWIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, RAWIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *rawImfileMetadataFromObject(const rawImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+rawImfileRow *rawImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+
+    return rawImfileRowAlloc(exp_id, class_id, uri, stats);
+}
+psArray *rawImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, RAWIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, RAWIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", RAWIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, rawImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long rawImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        rawImfileRow *object = objects->data[i];
+        psMetadata *where = rawImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, RAWIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from rawImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p1PendingExpRowFree(p1PendingExpRow *object);
+
+p1PendingExpRow *p1PendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version)
+{
+    p1PendingExpRow *object;
+
+    object = psAlloc(sizeof(p1PendingExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)p1PendingExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p1_version = p1_version;
+
+    return object;
+}
+
+static void p1PendingExpRowFree(p1PendingExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p1PendingExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P1PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P1PENDINGEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P1PENDINGEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p1PendingExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P1PENDINGEXP_TABLE_NAME);
+}
+
+bool p1PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P1PENDINGEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p1PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P1PENDINGEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P1PENDINGEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P1PENDINGEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P1PENDINGEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P1PENDINGEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p1PendingExpInsertObject(psDB *dbh, p1PendingExpRow *object)
+{
+    return p1PendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version);
+}
+
+p1PendingExpRow *p1PendingExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p1_version;
+
+    if (!p1PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p1PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version);
+}
+
+bool p1PendingExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P1PENDINGEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P1PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P1PENDINGEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P1PENDINGEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p1PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p1PendingExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P1PENDINGEXP_TABLE_NAME, P1PENDINGEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p1PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P1PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P1PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P1PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p1PendingExpMetadataFromObject(const p1PendingExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p1PendingExpRow *p1PendingExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p1_version = psMetadataLookupS32(&status, md, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        return false;
+    }
+
+    return p1PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version);
+}
+psArray *p1PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P1PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P1PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P1PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p1PendingExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p1PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p1PendingExpRow *object = objects->data[i];
+        psMetadata *where = p1PendingExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P1PENDINGEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p1PendingExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p2PendingExpRowFree(p2PendingExpRow *object);
+
+p2PendingExpRow *p2PendingExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    p2PendingExpRow *object;
+
+    object = psAlloc(sizeof(p2PendingExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)p2PendingExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p1_version = p1_version;
+    object->p2_version = p2_version;
+
+    return object;
+}
+
+static void p2PendingExpRowFree(p2PendingExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p2PendingExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P2PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2PENDINGEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P2PENDINGEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p2PendingExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P2PENDINGEXP_TABLE_NAME);
+}
+
+bool p2PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P2PENDINGEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p2PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P2PENDINGEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P2PENDINGEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P2PENDINGEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p2PendingExpInsertObject(psDB *dbh, p2PendingExpRow *object)
+{
+    return p2PendingExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version, object->p2_version);
+}
+
+p2PendingExpRow *p2PendingExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p1_version;
+    psS32           p2_version;
+
+    if (!p2PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p2PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
+}
+
+bool p2PendingExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P2PENDINGEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P2PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2PENDINGEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P2PENDINGEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p2PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p2PendingExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P2PENDINGEXP_TABLE_NAME, P2PENDINGEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p2PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P2PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P2PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p2PendingExpMetadataFromObject(const p2PendingExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p2PendingExpRow *p2PendingExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p1_version = psMetadataLookupS32(&status, md, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+
+    return p2PendingExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
+}
+psArray *p2PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P2PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p2PendingExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p2PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p2PendingExpRow *object = objects->data[i];
+        psMetadata *where = p2PendingExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P2PENDINGEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2PendingExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p2PendingImfileRowFree(p2PendingImfileRow *object);
+
+p2PendingImfileRow *p2PendingImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    p2PendingImfileRow *object;
+
+    object = psAlloc(sizeof(p2PendingImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)p2PendingImfileRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p1_version = p1_version;
+    object->p2_version = p2_version;
+
+    return object;
+}
+
+static void p2PendingImfileRowFree(p2PendingImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p2PendingImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P2PENDINGIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2PENDINGIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P2PENDINGIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p2PendingImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P2PENDINGIMFILE_TABLE_NAME);
+}
+
+bool p2PendingImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P2PENDINGIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p2PendingImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P2PENDINGIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2PENDINGIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P2PENDINGIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P2PENDINGIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p2PendingImfileInsertObject(psDB *dbh, p2PendingImfileRow *object)
+{
+    return p2PendingImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats, object->recipe, object->p1_version, object->p2_version);
+}
+
+p2PendingImfileRow *p2PendingImfilePopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p1_version;
+    psS32           p2_version;
+
+    if (!p2PendingImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p2PendingImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
+}
+
+bool p2PendingImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P2PENDINGIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P2PENDINGIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2PENDINGIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P2PENDINGIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p2PendingImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p2PendingImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P2PENDINGIMFILE_TABLE_NAME, P2PENDINGIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p2PendingImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P2PENDINGIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P2PENDINGIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p2PendingImfileMetadataFromObject(const p2PendingImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p2PendingImfileRow *p2PendingImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p1_version = psMetadataLookupS32(&status, md, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+
+    return p2PendingImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
+}
+psArray *p2PendingImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2PENDINGIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P2PENDINGIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2PENDINGIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p2PendingImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p2PendingImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p2PendingImfileRow *object = objects->data[i];
+        psMetadata *where = p2PendingImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P2PENDINGIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2PendingImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p2DoneExpRowFree(p2DoneExpRow *object);
+
+p2DoneExpRow *p2DoneExpRowAlloc(const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    p2DoneExpRow    *object;
+
+    object = psAlloc(sizeof(p2DoneExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)p2DoneExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p1_version = p1_version;
+    object->p2_version = p2_version;
+
+    return object;
+}
+
+static void p2DoneExpRowFree(p2DoneExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p2DoneExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P2DONEEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2DONEEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P2DONEEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p2DoneExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P2DONEEXP_TABLE_NAME);
+}
+
+bool p2DoneExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P2DONEEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p2DoneExpPop(psDB *dbh, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P2DONEEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P2DONEEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P2DONEEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P2DONEEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p2DoneExpInsertObject(psDB *dbh, p2DoneExpRow *object)
+{
+    return p2DoneExpInsert(dbh, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p1_version, object->p2_version);
+}
+
+p2DoneExpRow *p2DoneExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p1_version;
+    psS32           p2_version;
+
+    if (!p2DoneExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p2DoneExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
+}
+
+bool p2DoneExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P2DONEEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P2DONEEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2DONEEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P2DONEEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p2DoneExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p2DoneExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P2DONEEXP_TABLE_NAME, P2DONEEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p2DoneExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2DONEEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P2DONEEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P2DONEEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p2DoneExpMetadataFromObject(const p2DoneExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p2DoneExpRow *p2DoneExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p1_version = psMetadataLookupS32(&status, md, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+
+    return p2DoneExpRowAlloc(exp_id, camera, telescope, exp_type, imfiles, filter, stats, recipe, p1_version, p2_version);
+}
+psArray *p2DoneExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2DONEEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P2DONEEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p2DoneExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p2DoneExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p2DoneExpRow *object = objects->data[i];
+        psMetadata *where = p2DoneExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P2DONEEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2DoneExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p2DoneImfileRowFree(p2DoneImfileRow *object);
+
+p2DoneImfileRow *p2DoneImfileRowAlloc(const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    p2DoneImfileRow *object;
+
+    object = psAlloc(sizeof(p2DoneImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)p2DoneImfileRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p1_version = p1_version;
+    object->p2_version = p2_version;
+
+    return object;
+}
+
+static void p2DoneImfileRowFree(p2DoneImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p2DoneImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P2DONEIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P2DONEIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P2DONEIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p2DoneImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P2DONEIMFILE_TABLE_NAME);
+}
+
+bool p2DoneImfileInsert(psDB * dbh, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe, psS32 p1_version, psS32 p2_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P2DONEIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p2DoneImfilePop(psDB *dbh, char **exp_id, char **class_id, char **uri, char **stats, char **recipe, psS32 *p1_version, psS32 *p2_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P2DONEIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P2DONEIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P2DONEIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P2DONEIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p1_version = psMetadataLookupS32(&status, row, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p2DoneImfileInsertObject(psDB *dbh, p2DoneImfileRow *object)
+{
+    return p2DoneImfileInsert(dbh, object->exp_id, object->class_id, object->uri, object->stats, object->recipe, object->p1_version, object->p2_version);
+}
+
+p2DoneImfileRow *p2DoneImfilePopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p1_version;
+    psS32           p2_version;
+
+    if (!p2DoneImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p2DoneImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
+}
+
+bool p2DoneImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P2DONEIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P2DONEIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P2DONEIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P2DONEIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p2DoneImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p2DoneImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P2DONEIMFILE_TABLE_NAME, P2DONEIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p2DoneImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P2DONEIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P2DONEIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p2DoneImfileMetadataFromObject(const p2DoneImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, object->p1_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p1_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p2DoneImfileRow *p2DoneImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p1_version = psMetadataLookupS32(&status, md, "p1_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p1_version");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+
+    return p2DoneImfileRowAlloc(exp_id, class_id, uri, stats, recipe, p1_version, p2_version);
+}
+psArray *p2DoneImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P2DONEIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P2DONEIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P2DONEIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p2DoneImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p2DoneImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p2DoneImfileRow *object = objects->data[i];
+        psMetadata *where = p2DoneImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P2DONEIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p2DoneImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void p3PendingExpRowFree(p3PendingExpRow *object);
+
+p3PendingExpRow *p3PendingExpRowAlloc(const char *exp_id, const char *camera, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p2_version, psS32 p3_version)
+{
+    p3PendingExpRow *object;
+
+    object = psAlloc(sizeof(p3PendingExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)p3PendingExpRowFree);
+
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->p2_version = p2_version;
+    object->p3_version = p3_version;
+
+    return object;
+}
+
+static void p3PendingExpRowFree(p3PendingExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool p3PendingExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, P3PENDINGEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", P3PENDINGEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, P3PENDINGEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool p3PendingExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, P3PENDINGEXP_TABLE_NAME);
+}
+
+bool p3PendingExpInsert(psDB * dbh, const char *exp_id, const char *camera, const char *exp_type, psS32 imfiles, const char *filter, const char *stats, const char *recipe, psS32 p2_version, psS32 p3_version)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, P3PENDINGEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool p3PendingExpPop(psDB *dbh, char **exp_id, char **camera, char **exp_type, psS32 *imfiles, char **filter, char **stats, char **recipe, psS32 *p2_version, psS32 *p3_version)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PENDINGEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, P3PENDINGEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", P3PENDINGEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, P3PENDINGEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, P3PENDINGEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *p2_version = psMetadataLookupS32(&status, row, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        psFree(row);
+        return false;
+    }
+    *p3_version = psMetadataLookupS32(&status, row, "p3_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p3_version");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool p3PendingExpInsertObject(psDB *dbh, p3PendingExpRow *object)
+{
+    return p3PendingExpInsert(dbh, object->exp_id, object->camera, object->exp_type, object->imfiles, object->filter, object->stats, object->recipe, object->p2_version, object->p3_version);
+}
+
+p3PendingExpRow *p3PendingExpPopObject(psDB *dbh)
+{
+    char            exp_id[256];
+    char            camera[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+    char            recipe[256];
+    psS32           p2_version;
+    psS32           p3_version;
+
+    if (!p3PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p2_version, &p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return p3PendingExpRowAlloc(exp_id, camera, exp_type, imfiles, filter, stats, recipe, p2_version, p3_version);
+}
+
+bool p3PendingExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  P3PENDINGEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, P3PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", P3PENDINGEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, P3PENDINGEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool p3PendingExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!p3PendingExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                P3PENDINGEXP_TABLE_NAME, P3PENDINGEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool p3PendingExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, P3PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, P3PENDINGEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *p3PendingExpMetadataFromObject(const p3PendingExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, object->p2_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p2_version");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, object->p3_version)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item p3_version");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+p3PendingExpRow *p3PendingExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *exp_id;
+    char            *camera;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p2_version;
+    psS32           p3_version;
+
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    p2_version = psMetadataLookupS32(&status, md, "p2_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p2_version");
+        return false;
+    }
+    p3_version = psMetadataLookupS32(&status, md, "p3_version");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item p3_version");
+        return false;
+    }
+
+    return p3PendingExpRowAlloc(exp_id, camera, exp_type, imfiles, filter, stats, recipe, p2_version, p3_version);
+}
+psArray *p3PendingExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, P3PENDINGEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, P3PENDINGEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", P3PENDINGEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, p3PendingExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long p3PendingExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        p3PendingExpRow *object = objects->data[i];
+        psMetadata *where = p3PendingExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, P3PENDINGEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from p3PendingExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detRunRowFree(detRunRow *object);
+
+detRunRow *detRunRowAlloc(const char *det_type)
+{
+    detRunRow       *object;
+
+    object = psAlloc(sizeof(detRunRow));
+    psMemSetDeallocator(object, (psFreeFunc)detRunRowFree);
+
+    object->det_type = psStringCopy(det_type);
+
+    return object;
+}
+
+static void detRunRowFree(detRunRow *object)
+{
+    psFree(object->det_type);
+}
+
+bool detRunCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRUN_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRUN_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, "Key", "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETRUN_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detRunDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETRUN_TABLE_NAME);
+}
+
+bool detRunInsert(psDB * dbh, const char *det_type)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, NULL, psStringCopy(det_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETRUN_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detRunPop(psDB *dbh, char **det_type)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETRUN_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRUN_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETRUN_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRUN_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETRUN_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETRUN_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_type = psMetadataLookupPtr(&status, row, "det_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_type");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detRunInsertObject(psDB *dbh, detRunRow *object)
+{
+    return detRunInsert(dbh, object->det_type);
+}
+
+detRunRow *detRunPopObject(psDB *dbh)
+{
+    char            det_type[256];
+
+    if (!detRunPop(dbh, (char **)&det_type)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detRunRowAlloc(det_type);
+}
+
+bool detRunInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETRUN_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETRUN_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRUN_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETRUN_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detRunPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detRunSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETRUN_TABLE_NAME, DETRUN_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detRunSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRUN_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETRUN_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRUN_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETRUN_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detRunMetadataFromObject(const detRunRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, NULL, psStringCopy(object->det_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_type");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detRunRow *detRunObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    char            *det_type;
+
+    det_type = psMetadataLookupPtr(&status, md, "det_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_type");
+        return false;
+    }
+
+    return detRunRowAlloc(det_type);
+}
+psArray *detRunSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRUN_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETRUN_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRUN_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detRunObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detRunDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detRunRow *object = objects->data[i];
+        psMetadata *where = detRunMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETRUN_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detRun");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detInputExpRowFree(detInputExpRow *object);
+
+detInputExpRow *detInputExpRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    detInputExpRow  *object;
+
+    object = psAlloc(sizeof(detInputExpRow));
+    psMemSetDeallocator(object, (psFreeFunc)detInputExpRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->exp_id = psStringCopy(exp_id);
+    object->camera = psStringCopy(camera);
+    object->telescope = psStringCopy(telescope);
+    object->exp_type = psStringCopy(exp_type);
+    object->imfiles = imfiles;
+    object->filter = psStringCopy(filter);
+    object->stats = psStringCopy(stats);
+
+    return object;
+}
+
+static void detInputExpRowFree(detInputExpRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->camera);
+    psFree(object->telescope);
+    psFree(object->exp_type);
+    psFree(object->filter);
+    psFree(object->stats);
+}
+
+bool detInputExpCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETINPUTEXP_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETINPUTEXP_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETINPUTEXP_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detInputExpDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETINPUTEXP_TABLE_NAME);
+}
+
+bool detInputExpInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *camera, const char *telescope, const char *exp_type, psS32 imfiles, const char *filter, const char *stats)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETINPUTEXP_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detInputExpPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **camera, char **telescope, char **exp_type, psS32 *imfiles, char **filter, char **stats)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETINPUTEXP_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETINPUTEXP_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETINPUTEXP_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETINPUTEXP_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETINPUTEXP_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETINPUTEXP_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *camera = psMetadataLookupPtr(&status, row, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        psFree(row);
+        return false;
+    }
+    *telescope = psMetadataLookupPtr(&status, row, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        psFree(row);
+        return false;
+    }
+    *exp_type = psMetadataLookupPtr(&status, row, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        psFree(row);
+        return false;
+    }
+    *imfiles = psMetadataLookupS32(&status, row, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        psFree(row);
+        return false;
+    }
+    *filter = psMetadataLookupPtr(&status, row, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detInputExpInsertObject(psDB *dbh, detInputExpRow *object)
+{
+    return detInputExpInsert(dbh, object->det_id, object->iteration, object->exp_id, object->camera, object->telescope, object->exp_type, object->imfiles, object->filter, object->stats);
+}
+
+detInputExpRow *detInputExpPopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            exp_id[256];
+    char            camera[256];
+    char            telescope[256];
+    char            exp_type[256];
+    psS32           imfiles;
+    char            filter[256];
+    char            stats[256];
+
+    if (!detInputExpPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detInputExpRowAlloc(det_id, iteration, exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+
+bool detInputExpInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETINPUTEXP_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETINPUTEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETINPUTEXP_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETINPUTEXP_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detInputExpPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detInputExpSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETINPUTEXP_TABLE_NAME, DETINPUTEXP_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detInputExpSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETINPUTEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETINPUTEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETINPUTEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETINPUTEXP_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detInputExpMetadataFromObject(const detInputExpRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, psStringCopy(object->camera))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item camera");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, psStringCopy(object->telescope))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item telescope");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, psStringCopy(object->exp_type))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_type");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, object->imfiles)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item imfiles");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, psStringCopy(object->filter))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item filter");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detInputExpRow *detInputExpObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    camera = psMetadataLookupPtr(&status, md, "camera");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item camera");
+        return false;
+    }
+    telescope = psMetadataLookupPtr(&status, md, "telescope");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item telescope");
+        return false;
+    }
+    exp_type = psMetadataLookupPtr(&status, md, "exp_type");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_type");
+        return false;
+    }
+    imfiles = psMetadataLookupS32(&status, md, "imfiles");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item imfiles");
+        return false;
+    }
+    filter = psMetadataLookupPtr(&status, md, "filter");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item filter");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+
+    return detInputExpRowAlloc(det_id, iteration, exp_id, camera, telescope, exp_type, imfiles, filter, stats);
+}
+psArray *detInputExpSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETINPUTEXP_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETINPUTEXP_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETINPUTEXP_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detInputExpObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detInputExpDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detInputExpRow *object = objects->data[i];
+        psMetadata *where = detInputExpMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETINPUTEXP_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detInputExp");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detProcessedImfileRowFree(detProcessedImfileRow *object);
+
+detProcessedImfileRow *detProcessedImfileRowAlloc(psS32 det_id, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    detProcessedImfileRow *object;
+
+    object = psAlloc(sizeof(detProcessedImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)detProcessedImfileRowFree);
+
+    object->det_id = det_id;
+    object->exp_id = psStringCopy(exp_id);
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+
+    return object;
+}
+
+static void detProcessedImfileRowFree(detProcessedImfileRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detProcessedImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETPROCESSEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETPROCESSEDIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETPROCESSEDIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detProcessedImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETPROCESSEDIMFILE_TABLE_NAME);
+}
+
+bool detProcessedImfileInsert(psDB * dbh, psS32 det_id, const char *exp_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETPROCESSEDIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detProcessedImfilePop(psDB *dbh, psS32 *det_id, char **exp_id, char **class_id, char **uri, char **stats, char **recipe)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETPROCESSEDIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETPROCESSEDIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETPROCESSEDIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETPROCESSEDIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detProcessedImfileInsertObject(psDB *dbh, detProcessedImfileRow *object)
+{
+    return detProcessedImfileInsert(dbh, object->det_id, object->exp_id, object->class_id, object->uri, object->stats, object->recipe);
+}
+
+detProcessedImfileRow *detProcessedImfilePopObject(psDB *dbh)
+{
+    psS32           det_id;
+    char            exp_id[256];
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+
+    if (!detProcessedImfilePop(dbh, &det_id, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detProcessedImfileRowAlloc(det_id, exp_id, class_id, uri, stats, recipe);
+}
+
+bool detProcessedImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETPROCESSEDIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETPROCESSEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETPROCESSEDIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detProcessedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detProcessedImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETPROCESSEDIMFILE_TABLE_NAME, DETPROCESSEDIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detProcessedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETPROCESSEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETPROCESSEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETPROCESSEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detProcessedImfileMetadataFromObject(const detProcessedImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detProcessedImfileRow *detProcessedImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+
+    return detProcessedImfileRowAlloc(det_id, exp_id, class_id, uri, stats, recipe);
+}
+psArray *detProcessedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETPROCESSEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETPROCESSEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detProcessedImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detProcessedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detProcessedImfileRow *object = objects->data[i];
+        psMetadata *where = detProcessedImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETPROCESSEDIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detProcessedImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detStackedImfileRowFree(detStackedImfileRow *object);
+
+detStackedImfileRow *detStackedImfileRowAlloc(psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    detStackedImfileRow *object;
+
+    object = psAlloc(sizeof(detStackedImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)detStackedImfileRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+
+    return object;
+}
+
+static void detStackedImfileRowFree(detStackedImfileRow *object)
+{
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detStackedImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETSTACKEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETSTACKEDIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETSTACKEDIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detStackedImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETSTACKEDIMFILE_TABLE_NAME);
+}
+
+bool detStackedImfileInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETSTACKEDIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detStackedImfilePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **class_id, char **uri, char **stats, char **recipe)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETSTACKEDIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETSTACKEDIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETSTACKEDIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETSTACKEDIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detStackedImfileInsertObject(psDB *dbh, detStackedImfileRow *object)
+{
+    return detStackedImfileInsert(dbh, object->det_id, object->iteration, object->class_id, object->uri, object->stats, object->recipe);
+}
+
+detStackedImfileRow *detStackedImfilePopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+
+    if (!detStackedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detStackedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
+}
+
+bool detStackedImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETSTACKEDIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETSTACKEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETSTACKEDIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detStackedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detStackedImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETSTACKEDIMFILE_TABLE_NAME, DETSTACKEDIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detStackedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETSTACKEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETSTACKEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETSTACKEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detStackedImfileMetadataFromObject(const detStackedImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detStackedImfileRow *detStackedImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+
+    return detStackedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
+}
+psArray *detStackedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETSTACKEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETSTACKEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detStackedImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detStackedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detStackedImfileRow *object = objects->data[i];
+        psMetadata *where = detStackedImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETSTACKEDIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detStackedImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detNormalizedImfileRowFree(detNormalizedImfileRow *object);
+
+detNormalizedImfileRow *detNormalizedImfileRowAlloc(psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    detNormalizedImfileRow *object;
+
+    object = psAlloc(sizeof(detNormalizedImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)detNormalizedImfileRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+
+    return object;
+}
+
+static void detNormalizedImfileRowFree(detNormalizedImfileRow *object)
+{
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detNormalizedImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETNORMALIZEDIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detNormalizedImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETNORMALIZEDIMFILE_TABLE_NAME);
+}
+
+bool detNormalizedImfileInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detNormalizedImfilePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **class_id, char **uri, char **stats, char **recipe)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETNORMALIZEDIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETNORMALIZEDIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detNormalizedImfileInsertObject(psDB *dbh, detNormalizedImfileRow *object)
+{
+    return detNormalizedImfileInsert(dbh, object->det_id, object->iteration, object->class_id, object->uri, object->stats, object->recipe);
+}
+
+detNormalizedImfileRow *detNormalizedImfilePopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+
+    if (!detNormalizedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detNormalizedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
+}
+
+bool detNormalizedImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETNORMALIZEDIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETNORMALIZEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETNORMALIZEDIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detNormalizedImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detNormalizedImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETNORMALIZEDIMFILE_TABLE_NAME, DETNORMALIZEDIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detNormalizedImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETNORMALIZEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETNORMALIZEDIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detNormalizedImfileMetadataFromObject(const detNormalizedImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detNormalizedImfileRow *detNormalizedImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+
+    return detNormalizedImfileRowAlloc(det_id, iteration, class_id, uri, stats, recipe);
+}
+psArray *detNormalizedImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETNORMALIZEDIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETNORMALIZEDIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detNormalizedImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detNormalizedImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detNormalizedImfileRow *object = objects->data[i];
+        psMetadata *where = detNormalizedImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETNORMALIZEDIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detNormalizedImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detMasterFrameRowFree(detMasterFrameRow *object);
+
+detMasterFrameRow *detMasterFrameRowAlloc(psS32 det_id, psS32 iteration, const char *comment)
+{
+    detMasterFrameRow *object;
+
+    object = psAlloc(sizeof(detMasterFrameRow));
+    psMemSetDeallocator(object, (psFreeFunc)detMasterFrameRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->comment = psStringCopy(comment);
+
+    return object;
+}
+
+static void detMasterFrameRowFree(detMasterFrameRow *object)
+{
+    psFree(object->comment);
+}
+
+bool detMasterFrameCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETMASTERFRAME_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETMASTERFRAME_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETMASTERFRAME_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detMasterFrameDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETMASTERFRAME_TABLE_NAME);
+}
+
+bool detMasterFrameInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *comment)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, psStringCopy(comment))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETMASTERFRAME_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detMasterFramePop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **comment)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERFRAME_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETMASTERFRAME_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERFRAME_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETMASTERFRAME_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETMASTERFRAME_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *comment = psMetadataLookupPtr(&status, row, "comment");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item comment");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detMasterFrameInsertObject(psDB *dbh, detMasterFrameRow *object)
+{
+    return detMasterFrameInsert(dbh, object->det_id, object->iteration, object->comment);
+}
+
+detMasterFrameRow *detMasterFramePopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            comment[256];
+
+    if (!detMasterFramePop(dbh, &det_id, &iteration, (char **)&comment)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detMasterFrameRowAlloc(det_id, iteration, comment);
+}
+
+bool detMasterFrameInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETMASTERFRAME_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETMASTERFRAME_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETMASTERFRAME_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETMASTERFRAME_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detMasterFramePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detMasterFrameSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETMASTERFRAME_TABLE_NAME, DETMASTERFRAME_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detMasterFrameSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETMASTERFRAME_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERFRAME_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETMASTERFRAME_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detMasterFrameMetadataFromObject(const detMasterFrameRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, psStringCopy(object->comment))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item comment");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detMasterFrameRow *detMasterFrameObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *comment;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    comment = psMetadataLookupPtr(&status, md, "comment");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item comment");
+        return false;
+    }
+
+    return detMasterFrameRowAlloc(det_id, iteration, comment);
+}
+psArray *detMasterFrameSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERFRAME_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETMASTERFRAME_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERFRAME_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detMasterFrameObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detMasterFrameDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detMasterFrameRow *object = objects->data[i];
+        psMetadata *where = detMasterFrameMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETMASTERFRAME_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detMasterFrame");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detMasterImfileRowFree(detMasterImfileRow *object);
+
+detMasterImfileRow *detMasterImfileRowAlloc(psS32 det_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    detMasterImfileRow *object;
+
+    object = psAlloc(sizeof(detMasterImfileRow));
+    psMemSetDeallocator(object, (psFreeFunc)detMasterImfileRowFree);
+
+    object->det_id = det_id;
+    object->class_id = psStringCopy(class_id);
+    object->uri = psStringCopy(uri);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+
+    return object;
+}
+
+static void detMasterImfileRowFree(detMasterImfileRow *object)
+{
+    psFree(object->class_id);
+    psFree(object->uri);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detMasterImfileCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETMASTERIMFILE_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETMASTERIMFILE_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETMASTERIMFILE_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detMasterImfileDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETMASTERIMFILE_TABLE_NAME);
+}
+
+bool detMasterImfileInsert(psDB * dbh, psS32 det_id, const char *class_id, const char *uri, const char *stats, const char *recipe)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETMASTERIMFILE_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detMasterImfilePop(psDB *dbh, psS32 *det_id, char **class_id, char **uri, char **stats, char **recipe)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERIMFILE_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETMASTERIMFILE_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETMASTERIMFILE_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETMASTERIMFILE_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETMASTERIMFILE_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *uri = psMetadataLookupPtr(&status, row, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detMasterImfileInsertObject(psDB *dbh, detMasterImfileRow *object)
+{
+    return detMasterImfileInsert(dbh, object->det_id, object->class_id, object->uri, object->stats, object->recipe);
+}
+
+detMasterImfileRow *detMasterImfilePopObject(psDB *dbh)
+{
+    psS32           det_id;
+    char            class_id[256];
+    char            uri[256];
+    char            stats[256];
+    char            recipe[256];
+
+    if (!detMasterImfilePop(dbh, &det_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detMasterImfileRowAlloc(det_id, class_id, uri, stats, recipe);
+}
+
+bool detMasterImfileInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETMASTERIMFILE_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETMASTERIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETMASTERIMFILE_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETMASTERIMFILE_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detMasterImfilePopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detMasterImfileSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETMASTERIMFILE_TABLE_NAME, DETMASTERIMFILE_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detMasterImfileSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETMASTERIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETMASTERIMFILE_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detMasterImfileMetadataFromObject(const detMasterImfileRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, psStringCopy(object->uri))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item uri");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detMasterImfileRow *detMasterImfileObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    uri = psMetadataLookupPtr(&status, md, "uri");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item uri");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+
+    return detMasterImfileRowAlloc(det_id, class_id, uri, stats, recipe);
+}
+psArray *detMasterImfileSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETMASTERIMFILE_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETMASTERIMFILE_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETMASTERIMFILE_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detMasterImfileObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detMasterImfileDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detMasterImfileRow *object = objects->data[i];
+        psMetadata *where = detMasterImfileMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETMASTERIMFILE_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detMasterImfile");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detResidImfileAnalysisRowFree(detResidImfileAnalysisRow *object);
+
+detResidImfileAnalysisRow *detResidImfileAnalysisRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *class_id, const char *stats, const char *recipe)
+{
+    detResidImfileAnalysisRow *object;
+
+    object = psAlloc(sizeof(detResidImfileAnalysisRow));
+    psMemSetDeallocator(object, (psFreeFunc)detResidImfileAnalysisRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->exp_id = psStringCopy(exp_id);
+    object->class_id = psStringCopy(class_id);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+
+    return object;
+}
+
+static void detResidImfileAnalysisRowFree(detResidImfileAnalysisRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->class_id);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detResidImfileAnalysisCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRESIDIMFILEANALYSIS_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detResidImfileAnalysisDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME);
+}
+
+bool detResidImfileAnalysisInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *class_id, const char *stats, const char *recipe)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detResidImfileAnalysisPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **class_id, char **stats, char **recipe)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETRESIDIMFILEANALYSIS_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETRESIDIMFILEANALYSIS_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *class_id = psMetadataLookupPtr(&status, row, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detResidImfileAnalysisInsertObject(psDB *dbh, detResidImfileAnalysisRow *object)
+{
+    return detResidImfileAnalysisInsert(dbh, object->det_id, object->iteration, object->exp_id, object->class_id, object->stats, object->recipe);
+}
+
+detResidImfileAnalysisRow *detResidImfileAnalysisPopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            exp_id[256];
+    char            class_id[256];
+    char            stats[256];
+    char            recipe[256];
+
+    if (!detResidImfileAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&class_id, (char **)&stats, (char **)&recipe)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detResidImfileAnalysisRowAlloc(det_id, iteration, exp_id, class_id, stats, recipe);
+}
+
+bool detResidImfileAnalysisInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETRESIDIMFILEANALYSIS_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETRESIDIMFILEANALYSIS_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRESIDIMFILEANALYSIS_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detResidImfileAnalysisPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detResidImfileAnalysisSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETRESIDIMFILEANALYSIS_TABLE_NAME, DETRESIDIMFILEANALYSIS_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detResidImfileAnalysisSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETRESIDIMFILEANALYSIS_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETRESIDIMFILEANALYSIS_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detResidImfileAnalysisMetadataFromObject(const detResidImfileAnalysisRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, psStringCopy(object->class_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item class_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detResidImfileAnalysisRow *detResidImfileAnalysisObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *class_id;
+    char            *stats;
+    char            *recipe;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    class_id = psMetadataLookupPtr(&status, md, "class_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item class_id");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+
+    return detResidImfileAnalysisRowAlloc(det_id, iteration, exp_id, class_id, stats, recipe);
+}
+psArray *detResidImfileAnalysisSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETRESIDIMFILEANALYSIS_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDIMFILEANALYSIS_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detResidImfileAnalysisObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detResidImfileAnalysisDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detResidImfileAnalysisRow *object = objects->data[i];
+        psMetadata *where = detResidImfileAnalysisMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETRESIDIMFILEANALYSIS_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detResidImfileAnalysis");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
+static void detResidExpAnalysisRowFree(detResidExpAnalysisRow *object);
+
+detResidExpAnalysisRow *detResidExpAnalysisRowAlloc(psS32 det_id, psS32 iteration, const char *exp_id, const char *stats, const char *recipe, bool accept)
+{
+    detResidExpAnalysisRow *object;
+
+    object = psAlloc(sizeof(detResidExpAnalysisRow));
+    psMemSetDeallocator(object, (psFreeFunc)detResidExpAnalysisRowFree);
+
+    object->det_id = det_id;
+    object->iteration = iteration;
+    object->exp_id = psStringCopy(exp_id);
+    object->stats = psStringCopy(stats);
+    object->recipe = psStringCopy(recipe);
+    object->accept = accept;
+
+    return object;
+}
+
+static void detResidExpAnalysisRowFree(detResidExpAnalysisRow *object)
+{
+    psFree(object->exp_id);
+    psFree(object->stats);
+    psFree(object->recipe);
+}
+
+bool detResidExpAnalysisCreateTable(psDB *dbh)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAdd(md, PS_LIST_TAIL, DETRESIDEXPANALYSIS_INDEX_NAME, PS_DATA_S32, "AUTO_INCREMENT", 0.0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, "Primary Key", 0)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, "Primary Key", "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "255")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "64")) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, false)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBCreateTable(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, md);
+
+    psFree(md);
+
+    return status;
+}
+
+bool detResidExpAnalysisDropTable(psDB *dbh)
+{
+    return psDBDropTable(dbh, DETRESIDEXPANALYSIS_TABLE_NAME);
+}
+
+bool detResidExpAnalysisInsert(psDB * dbh, psS32 det_id, psS32 iteration, const char *exp_id, const char *stats, const char *recipe, bool accept)
+{
+    psMetadata      *md;
+    bool            status;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return false;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, accept)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
+        psFree(md);
+        return false;
+    }
+
+    status = psDBInsertOneRow(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, md);
+    psFree(md);
+
+    return status;
+}
+
+bool detResidExpAnalysisPop(psDB *dbh, psS32 *det_id, psS32 *iteration, char **exp_id, char **stats, char **recipe, bool *accept)
+{
+    psArray         *rowSet;
+    psMetadata      *row;
+    psMetadata      *popped;
+    long            deleted;
+    bool            status;
+    int             rowID;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, NULL, 1);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
+        psFree(rowSet);
+        return NULL;
+    }
+
+    row = psArrayGet(rowSet, 0);
+    psMemIncrRefCounter(row);
+    if (!row) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return NULL;
+    }
+    psFree(rowSet);
+
+    rowID = psMetadataLookupS32(&status, row, DETRESIDEXPANALYSIS_INDEX_NAME);
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
+        psFree(row);
+        return NULL;
+    }
+
+    popped = psMetadataAlloc();
+    psMetadataAddS32(popped, PS_LIST_TAIL, DETRESIDEXPANALYSIS_INDEX_NAME, 0, NULL, rowID);
+
+    deleted = psDBDeleteRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, popped, 0);
+    if (deleted != 1) {
+        psError(PS_ERR_UNKNOWN, false, "database failed to delete row");
+        psFree(popped);
+        psFree(row);
+        return NULL;
+    }
+
+    psFree(popped);
+
+    *det_id = psMetadataLookupS32(&status, row, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        psFree(row);
+        return false;
+    }
+    *iteration = psMetadataLookupS32(&status, row, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        psFree(row);
+        return false;
+    }
+    *exp_id = psMetadataLookupPtr(&status, row, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        psFree(row);
+        return false;
+    }
+    *stats = psMetadataLookupPtr(&status, row, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        psFree(row);
+        return false;
+    }
+    *recipe = psMetadataLookupPtr(&status, row, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        psFree(row);
+        return false;
+    }
+    *accept = psMetadataLookupBool(&status, row, "accept");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item accept");
+        psFree(row);
+        return false;
+    }
+
+    psFree(row);
+
+    return true;
+}
+
+bool detResidExpAnalysisInsertObject(psDB *dbh, detResidExpAnalysisRow *object)
+{
+    return detResidExpAnalysisInsert(dbh, object->det_id, object->iteration, object->exp_id, object->stats, object->recipe, object->accept);
+}
+
+detResidExpAnalysisRow *detResidExpAnalysisPopObject(psDB *dbh)
+{
+    psS32           det_id;
+    psS32           iteration;
+    char            exp_id[256];
+    char            stats[256];
+    char            recipe[256];
+    bool            accept;
+
+    if (!detResidExpAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&stats, (char **)&recipe, &accept)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to pop a database row");
+        return NULL;
+    }
+
+    return detResidExpAnalysisRowAlloc(det_id, iteration, exp_id, stats, recipe, accept);
+}
+
+bool detResidExpAnalysisInsertFits(psDB *dbh, const psFits *fits)
+{
+    psArray         *rowSet;
+
+    // move to (the first?) extension named  DETRESIDEXPANALYSIS_TABLE_NAME
+    if (!psFitsMoveExtName(fits, DETRESIDEXPANALYSIS_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, true, "failed to find FITS extension %s", DETRESIDEXPANALYSIS_TABLE_NAME);
+        return false;
+    }
+
+    // check HDU type
+    if (psFitsGetExtType(fits) != PS_FITS_TYPE_BINARY_TABLE)  {
+        psError(PS_ERR_UNKNOWN, true, "FITS HDU type is not PS_FITS_TYPE_BINARY_TABLE");
+        return false;
+    }
+
+    // read fits table
+    rowSet = psFitsReadTable(fits);
+    if (!rowSet) {
+        psError(PS_ERR_UNKNOWN, true, "FITS read error or FITS table is empty");
+        psFree(rowSet);
+        return false;
+    }
+
+    if (!psDBInsertRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, rowSet)) {
+        psError(PS_ERR_UNKNOWN, false, "databse insert failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+bool detResidExpAnalysisPopFits(psDB *dbh, psFits *fits, unsigned long long limit)
+{
+    char            query[MAX_STRING_LENGTH];
+
+    if (!detResidExpAnalysisSelectRowsFits(dbh, fits, NULL, limit)) {
+        psError(PS_ERR_UNKNOWN, true, "database error or table is empty");
+        return false;
+    }
+
+    // remove limit rows from the end of the database
+    if (snprintf(query, MAX_STRING_LENGTH,
+                "DELETE FROM %s ORDER BY %s DESC LIMIT %lld",
+                DETRESIDEXPANALYSIS_TABLE_NAME, DETRESIDEXPANALYSIS_INDEX_NAME, limit) < 0) {
+        psError(PS_ERR_UNKNOWN, true, "query value attempted to exceed %s bytes", MAX_STRING_LENGTH);
+        return false;
+    }
+            
+    if (!p_psDBRunQuery(dbh, query)) {
+        psError(PS_ERR_UNKNOWN, false, "database query failed");
+        return false;
+    }
+
+    return true;
+}
+
+bool detResidExpAnalysisSelectRowsFits(psDB *dbh, psFits *fits, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return false;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)rowSet->data[i], 0, DETRESIDEXPANALYSIS_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // output to fits
+    if (!psFitsWriteTable(fits, NULL, rowSet, DETRESIDEXPANALYSIS_TABLE_NAME)) {
+        psError(PS_ERR_UNKNOWN, false, "FITS table write failed");
+        psFree(rowSet);
+        return false;
+    }
+
+    psFree(rowSet);
+
+    return true;
+}
+
+psMetadata *detResidExpAnalysisMetadataFromObject(const detResidExpAnalysisRow *object)
+{
+    psMetadata      *md;
+
+    md = psMetadataAlloc();
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, object->det_id)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item det_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, object->iteration)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item iteration");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, psStringCopy(object->exp_id))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item exp_id");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, psStringCopy(object->stats))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item stats");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, psStringCopy(object->recipe))) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item recipe");
+        psFree(md);
+        return NULL;
+    }
+    if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, object->accept)) {
+        psError(PS_ERR_UNKNOWN, false, "failed to add item accept");
+        psFree(md);
+        return NULL;
+    }
+
+    return md;
+}
+
+detResidExpAnalysisRow *detResidExpAnalysisObjectFromMetadata(psMetadata *md)
+{
+    bool            status;
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *stats;
+    char            *recipe;
+    bool            accept;
+
+    det_id = psMetadataLookupS32(&status, md, "det_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item det_id");
+        return false;
+    }
+    iteration = psMetadataLookupS32(&status, md, "iteration");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item iteration");
+        return false;
+    }
+    exp_id = psMetadataLookupPtr(&status, md, "exp_id");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item exp_id");
+        return false;
+    }
+    stats = psMetadataLookupPtr(&status, md, "stats");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item stats");
+        return false;
+    }
+    recipe = psMetadataLookupPtr(&status, md, "recipe");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item recipe");
+        return false;
+    }
+    accept = psMetadataLookupBool(&status, md, "accept");
+    if (!status) {
+        psError(PS_ERR_UNKNOWN, true, "failed to lookup value for item accept");
+        return false;
+    }
+
+    return detResidExpAnalysisRowAlloc(det_id, iteration, exp_id, stats, recipe, accept);
+}
+psArray *detResidExpAnalysisSelectRowObjects(psDB *dbh, const psMetadata *where, unsigned long long limit)
+{
+    psArray         *rowSet;
+    psArray         *returnSet;
+    psU64           i;
+
+    rowSet = psDBSelectRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, where, limit);
+    if (!rowSet) {
+        return NULL;
+    }
+
+    // strip index column
+    for (i = 0; i < rowSet->n; i++) {
+        if (!psMetadataRemove((psMetadata *)(rowSet->data[i]), 0, DETRESIDEXPANALYSIS_INDEX_NAME)) {
+            psError(PS_ERR_UNKNOWN, true, "failed to remove item %s", DETRESIDEXPANALYSIS_INDEX_NAME);
+            psFree(rowSet);
+            return false;
+        }
+    }
+
+    // convert psMetadata rows to row objects
+
+    returnSet = psArrayAlloc(rowSet->n);
+    returnSet->n = 0;
+
+    for (i = 0; i < rowSet->n; i++) {
+        psArrayAdd(returnSet, 0, detResidExpAnalysisObjectFromMetadata(rowSet->data[i]));
+    }
+
+    psFree(rowSet);
+
+    return returnSet;
+}
+long long detResidExpAnalysisDeleteRowObjects(psDB *dbh, const psArray *objects, unsigned long long limit)
+{
+    long long       deleted = 0;
+
+    for (long long i = 0; i < objects->n; i++) {
+        detResidExpAnalysisRow *object = objects->data[i];
+        psMetadata *where = detResidExpAnalysisMetadataFromObject(object);
+        long long count = psDBDeleteRows(dbh, DETRESIDEXPANALYSIS_TABLE_NAME, where, limit);
+        psFree(where)
+        if (count < 0) {
+            psError(PS_ERR_UNKNOWN, true, "failed to delete row from detResidExpAnalysis");
+            return count;
+        }
+
+        deleted += count;
+    }
+
+    return deleted;
+}
Index: /tags/rel-0_0_1/ippdb/src/ippdb.h
===================================================================
--- /tags/rel-0_0_1/ippdb/src/ippdb.h	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/src/ippdb.h	(revision 7462)
@@ -0,0 +1,5869 @@
+#ifndef IPPDB_H
+#define IPPDB_H 1
+
+#include <pslib.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/// @addtogroup ippdb
+/// @{
+
+/** Opens a new database connection
+ *
+ *  @return A new psDB object if the database connection is successful or NULL
+ *  on failure.
+ */
+
+psDB *ippdbInit(
+    const char      *host,              ///< Database server hostname
+    const char      *user,              ///< Database username
+    const char      *passwd,            ///< Database password
+    const char      *dbname             ///< Database table.namespace
+);
+
+/** Closes a database connection
+ */
+
+void ippdbCleanup(
+    psDB            *dbh                ///< Database handle
+);
+
+/** weatherRow data structure
+ *
+ * Structure for representing a single row of weather table data.
+ */
+
+typedef struct {
+    psF32           temp01;
+    psF32           humi01;
+    psF32           temp02;
+    psF32           humi02;
+    psF32           temp03;
+    psF32           humi03;
+    psF32           pressure;
+} weatherRow;
+
+/** Creates a new weatherRow object
+ *
+ *  @return A new weatherRow object or NULL on failure.
+ */
+
+weatherRow *weatherRowAlloc(
+    psF32           temp01,
+    psF32           humi01,
+    psF32           temp02,
+    psF32           humi02,
+    psF32           temp03,
+    psF32           humi03,
+    psF32           pressure
+);
+
+/** Creates a new weather table
+ *
+ * @return true on success
+ */
+
+bool weatherCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a weather table
+ *
+ * @return true on success
+ */
+
+bool weatherDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool weatherInsert(
+    psDB            *dbh,               ///< Database handle
+    psF32           temp01,
+    psF32           humi01,
+    psF32           temp02,
+    psF32           humi02,
+    psF32           temp03,
+    psF32           humi03,
+    psF32           pressure
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool weatherPop(
+    psDB            *dbh,               ///< Database handle
+    psF32           *temp01,
+    psF32           *humi01,
+    psF32           *temp02,
+    psF32           *humi02,
+    psF32           *temp03,
+    psF32           *humi03,
+    psF32           *pressure
+);
+
+/** Insert a single weatherRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool weatherInsertObject(
+    psDB            *dbh,               ///< Database handle
+    weatherRow      *object             ///< weatherRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new weatherRow on success or NULL on failure.
+ */
+
+weatherRow *weatherPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table weatherRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool weatherInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool weatherPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool weatherSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a weatherRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *weatherMetadataFromObject(
+    const weatherRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A weatherRow pointer or NULL on error
+ */
+
+weatherRow *weatherObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as weatherRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *weatherSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long weatherDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** skyp_transparencyRow data structure
+ *
+ * Structure for representing a single row of skyp_transparency table data.
+ */
+
+typedef struct {
+    char            *filter;
+    psF64           trans;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+} skyp_transparencyRow;
+
+/** Creates a new skyp_transparencyRow object
+ *
+ *  @return A new skyp_transparencyRow object or NULL on failure.
+ */
+
+skyp_transparencyRow *skyp_transparencyRowAlloc(
+    const char      *filter,
+    psF64           trans,
+    psS32           nstars,
+    psF64           ra,
+    psF64           decl,
+    psF32           exptime,
+    psF64           sky_bright
+);
+
+/** Creates a new skyp_transparency table
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a skyp_transparency table
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *filter,
+    psF64           trans,
+    psS32           nstars,
+    psF64           ra,
+    psF64           decl,
+    psF32           exptime,
+    psF64           sky_bright
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyPop(
+    psDB            *dbh,               ///< Database handle
+    char            **filter,
+    psF64           *trans,
+    psS32           *nstars,
+    psF64           *ra,
+    psF64           *decl,
+    psF32           *exptime,
+    psF64           *sky_bright
+);
+
+/** Insert a single skyp_transparencyRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyInsertObject(
+    psDB            *dbh,               ///< Database handle
+    skyp_transparencyRow *object             ///< skyp_transparencyRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new skyp_transparencyRow on success or NULL on failure.
+ */
+
+skyp_transparencyRow *skyp_transparencyPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table skyp_transparencyRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencyPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool skyp_transparencySelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a skyp_transparencyRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *skyp_transparencyMetadataFromObject(
+    const skyp_transparencyRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A skyp_transparencyRow pointer or NULL on error
+ */
+
+skyp_transparencyRow *skyp_transparencyObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as skyp_transparencyRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *skyp_transparencySelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long skyp_transparencyDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** skyp_absorptionRow data structure
+ *
+ * Structure for representing a single row of skyp_absorption table data.
+ */
+
+typedef struct {
+    char            *disperser_id;
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psS32           nstars;
+    psF64           ra;
+    psF64           decl;
+    psF32           exptime;
+    psF64           sky_bright;
+} skyp_absorptionRow;
+
+/** Creates a new skyp_absorptionRow object
+ *
+ *  @return A new skyp_absorptionRow object or NULL on failure.
+ */
+
+skyp_absorptionRow *skyp_absorptionRowAlloc(
+    const char      *disperser_id,
+    psF32           atmcomp1,
+    psF32           atmcomp2,
+    psF32           atmcomp3,
+    psS32           nstars,
+    psF64           ra,
+    psF64           decl,
+    psF32           exptime,
+    psF64           sky_bright
+);
+
+/** Creates a new skyp_absorption table
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a skyp_absorption table
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *disperser_id,
+    psF32           atmcomp1,
+    psF32           atmcomp2,
+    psF32           atmcomp3,
+    psS32           nstars,
+    psF64           ra,
+    psF64           decl,
+    psF32           exptime,
+    psF64           sky_bright
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionPop(
+    psDB            *dbh,               ///< Database handle
+    char            **disperser_id,
+    psF32           *atmcomp1,
+    psF32           *atmcomp2,
+    psF32           *atmcomp3,
+    psS32           *nstars,
+    psF64           *ra,
+    psF64           *decl,
+    psF32           *exptime,
+    psF64           *sky_bright
+);
+
+/** Insert a single skyp_absorptionRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionInsertObject(
+    psDB            *dbh,               ///< Database handle
+    skyp_absorptionRow *object             ///< skyp_absorptionRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new skyp_absorptionRow on success or NULL on failure.
+ */
+
+skyp_absorptionRow *skyp_absorptionPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table skyp_absorptionRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool skyp_absorptionSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a skyp_absorptionRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *skyp_absorptionMetadataFromObject(
+    const skyp_absorptionRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A skyp_absorptionRow pointer or NULL on error
+ */
+
+skyp_absorptionRow *skyp_absorptionObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as skyp_absorptionRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *skyp_absorptionSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long skyp_absorptionDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** skyp_emissionRow data structure
+ *
+ * Structure for representing a single row of skyp_emission table data.
+ */
+
+typedef struct {
+    char            *disperser_id;
+    psF32           atmcomp1;
+    psF32           atmcomp2;
+    psF32           atmcomp3;
+    psF32           continuum;
+    psF32           exptime;
+} skyp_emissionRow;
+
+/** Creates a new skyp_emissionRow object
+ *
+ *  @return A new skyp_emissionRow object or NULL on failure.
+ */
+
+skyp_emissionRow *skyp_emissionRowAlloc(
+    const char      *disperser_id,
+    psF32           atmcomp1,
+    psF32           atmcomp2,
+    psF32           atmcomp3,
+    psF32           continuum,
+    psF32           exptime
+);
+
+/** Creates a new skyp_emission table
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a skyp_emission table
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *disperser_id,
+    psF32           atmcomp1,
+    psF32           atmcomp2,
+    psF32           atmcomp3,
+    psF32           continuum,
+    psF32           exptime
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionPop(
+    psDB            *dbh,               ///< Database handle
+    char            **disperser_id,
+    psF32           *atmcomp1,
+    psF32           *atmcomp2,
+    psF32           *atmcomp3,
+    psF32           *continuum,
+    psF32           *exptime
+);
+
+/** Insert a single skyp_emissionRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionInsertObject(
+    psDB            *dbh,               ///< Database handle
+    skyp_emissionRow *object             ///< skyp_emissionRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new skyp_emissionRow on success or NULL on failure.
+ */
+
+skyp_emissionRow *skyp_emissionPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table skyp_emissionRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool skyp_emissionSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a skyp_emissionRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *skyp_emissionMetadataFromObject(
+    const skyp_emissionRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A skyp_emissionRow pointer or NULL on error
+ */
+
+skyp_emissionRow *skyp_emissionObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as skyp_emissionRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *skyp_emissionSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long skyp_emissionDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** dimmRow data structure
+ *
+ * Structure for representing a single row of dimm table data.
+ */
+
+typedef struct {
+    psF32           sigmax;
+    psF32           sigmay;
+    psF32           fwhm;
+    psF64           ra;
+    psF64           decl;
+    psF32           expttime;
+    char            *telescope_id;
+} dimmRow;
+
+/** Creates a new dimmRow object
+ *
+ *  @return A new dimmRow object or NULL on failure.
+ */
+
+dimmRow *dimmRowAlloc(
+    psF32           sigmax,
+    psF32           sigmay,
+    psF32           fwhm,
+    psF64           ra,
+    psF64           decl,
+    psF32           expttime,
+    const char      *telescope_id
+);
+
+/** Creates a new dimm table
+ *
+ * @return true on success
+ */
+
+bool dimmCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a dimm table
+ *
+ * @return true on success
+ */
+
+bool dimmDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool dimmInsert(
+    psDB            *dbh,               ///< Database handle
+    psF32           sigmax,
+    psF32           sigmay,
+    psF32           fwhm,
+    psF64           ra,
+    psF64           decl,
+    psF32           expttime,
+    const char      *telescope_id
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool dimmPop(
+    psDB            *dbh,               ///< Database handle
+    psF32           *sigmax,
+    psF32           *sigmay,
+    psF32           *fwhm,
+    psF64           *ra,
+    psF64           *decl,
+    psF32           *expttime,
+    char            **telescope_id
+);
+
+/** Insert a single dimmRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool dimmInsertObject(
+    psDB            *dbh,               ///< Database handle
+    dimmRow         *object             ///< dimmRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new dimmRow on success or NULL on failure.
+ */
+
+dimmRow *dimmPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table dimmRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool dimmInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool dimmPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool dimmSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a dimmRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *dimmMetadataFromObject(
+    const dimmRow   *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A dimmRow pointer or NULL on error
+ */
+
+dimmRow *dimmObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as dimmRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *dimmSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long dimmDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** skyp_irRow data structure
+ *
+ * Structure for representing a single row of skyp_ir table data.
+ */
+
+typedef struct {
+    psF64           sky_bright;
+    psF64           sky_var;
+    psF64           ra;
+    psF64           decl;
+    psF32           fov_x;
+    psF32           fov_y;
+} skyp_irRow;
+
+/** Creates a new skyp_irRow object
+ *
+ *  @return A new skyp_irRow object or NULL on failure.
+ */
+
+skyp_irRow *skyp_irRowAlloc(
+    psF64           sky_bright,
+    psF64           sky_var,
+    psF64           ra,
+    psF64           decl,
+    psF32           fov_x,
+    psF32           fov_y
+);
+
+/** Creates a new skyp_ir table
+ *
+ * @return true on success
+ */
+
+bool skyp_irCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a skyp_ir table
+ *
+ * @return true on success
+ */
+
+bool skyp_irDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_irInsert(
+    psDB            *dbh,               ///< Database handle
+    psF64           sky_bright,
+    psF64           sky_var,
+    psF64           ra,
+    psF64           decl,
+    psF32           fov_x,
+    psF32           fov_y
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool skyp_irPop(
+    psDB            *dbh,               ///< Database handle
+    psF64           *sky_bright,
+    psF64           *sky_var,
+    psF64           *ra,
+    psF64           *decl,
+    psF32           *fov_x,
+    psF32           *fov_y
+);
+
+/** Insert a single skyp_irRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool skyp_irInsertObject(
+    psDB            *dbh,               ///< Database handle
+    skyp_irRow      *object             ///< skyp_irRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new skyp_irRow on success or NULL on failure.
+ */
+
+skyp_irRow *skyp_irPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table skyp_irRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool skyp_irInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool skyp_irPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool skyp_irSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a skyp_irRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *skyp_irMetadataFromObject(
+    const skyp_irRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A skyp_irRow pointer or NULL on error
+ */
+
+skyp_irRow *skyp_irObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as skyp_irRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *skyp_irSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long skyp_irDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** domeRow data structure
+ *
+ * Structure for representing a single row of dome table data.
+ */
+
+typedef struct {
+    psF32           az;
+    bool            open;
+    bool            light;
+    bool            track;
+} domeRow;
+
+/** Creates a new domeRow object
+ *
+ *  @return A new domeRow object or NULL on failure.
+ */
+
+domeRow *domeRowAlloc(
+    psF32           az,
+    bool            open,
+    bool            light,
+    bool            track
+);
+
+/** Creates a new dome table
+ *
+ * @return true on success
+ */
+
+bool domeCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a dome table
+ *
+ * @return true on success
+ */
+
+bool domeDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool domeInsert(
+    psDB            *dbh,               ///< Database handle
+    psF32           az,
+    bool            open,
+    bool            light,
+    bool            track
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool domePop(
+    psDB            *dbh,               ///< Database handle
+    psF32           *az,
+    bool            *open,
+    bool            *light,
+    bool            *track
+);
+
+/** Insert a single domeRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool domeInsertObject(
+    psDB            *dbh,               ///< Database handle
+    domeRow         *object             ///< domeRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new domeRow on success or NULL on failure.
+ */
+
+domeRow *domePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table domeRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool domeInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool domePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool domeSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a domeRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *domeMetadataFromObject(
+    const domeRow   *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A domeRow pointer or NULL on error
+ */
+
+domeRow *domeObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as domeRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *domeSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long domeDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** telescopeRow data structure
+ *
+ * Structure for representing a single row of telescope table data.
+ */
+
+typedef struct {
+    char            *guide;
+    psF32           alt;
+    psF32           az;
+    psF64           ra;
+    psF64           decl;
+} telescopeRow;
+
+/** Creates a new telescopeRow object
+ *
+ *  @return A new telescopeRow object or NULL on failure.
+ */
+
+telescopeRow *telescopeRowAlloc(
+    const char      *guide,
+    psF32           alt,
+    psF32           az,
+    psF64           ra,
+    psF64           decl
+);
+
+/** Creates a new telescope table
+ *
+ * @return true on success
+ */
+
+bool telescopeCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a telescope table
+ *
+ * @return true on success
+ */
+
+bool telescopeDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool telescopeInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *guide,
+    psF32           alt,
+    psF32           az,
+    psF64           ra,
+    psF64           decl
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool telescopePop(
+    psDB            *dbh,               ///< Database handle
+    char            **guide,
+    psF32           *alt,
+    psF32           *az,
+    psF64           *ra,
+    psF64           *decl
+);
+
+/** Insert a single telescopeRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool telescopeInsertObject(
+    psDB            *dbh,               ///< Database handle
+    telescopeRow    *object             ///< telescopeRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new telescopeRow on success or NULL on failure.
+ */
+
+telescopeRow *telescopePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table telescopeRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool telescopeInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool telescopePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool telescopeSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a telescopeRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *telescopeMetadataFromObject(
+    const telescopeRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A telescopeRow pointer or NULL on error
+ */
+
+telescopeRow *telescopeObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as telescopeRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *telescopeSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long telescopeDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** summitExpRow data structure
+ *
+ * Structure for representing a single row of summitExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    char            *uri;
+} summitExpRow;
+
+/** Creates a new summitExpRow object
+ *
+ *  @return A new summitExpRow object or NULL on failure.
+ */
+
+summitExpRow *summitExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    const char      *uri
+);
+
+/** Creates a new summitExp table
+ *
+ * @return true on success
+ */
+
+bool summitExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a summitExp table
+ *
+ * @return true on success
+ */
+
+bool summitExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool summitExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    const char      *uri
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool summitExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    char            **uri
+);
+
+/** Insert a single summitExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool summitExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    summitExpRow    *object             ///< summitExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new summitExpRow on success or NULL on failure.
+ */
+
+summitExpRow *summitExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table summitExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool summitExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool summitExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool summitExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a summitExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *summitExpMetadataFromObject(
+    const summitExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A summitExpRow pointer or NULL on error
+ */
+
+summitExpRow *summitExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as summitExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *summitExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long summitExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** pzPendingExpRow data structure
+ *
+ * Structure for representing a single row of pzPendingExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+} pzPendingExpRow;
+
+/** Creates a new pzPendingExpRow object
+ *
+ *  @return A new pzPendingExpRow object or NULL on failure.
+ */
+
+pzPendingExpRow *pzPendingExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles
+);
+
+/** Creates a new pzPendingExp table
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a pzPendingExp table
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles
+);
+
+/** Insert a single pzPendingExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    pzPendingExpRow *object             ///< pzPendingExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new pzPendingExpRow on success or NULL on failure.
+ */
+
+pzPendingExpRow *pzPendingExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table pzPendingExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool pzPendingExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a pzPendingExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *pzPendingExpMetadataFromObject(
+    const pzPendingExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A pzPendingExpRow pointer or NULL on error
+ */
+
+pzPendingExpRow *pzPendingExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as pzPendingExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *pzPendingExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long pzPendingExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** pzPendingImfileRow data structure
+ *
+ * Structure for representing a single row of pzPendingImfile table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    psS32           bytes;
+    char            *md5sum;
+    char            *class;
+    char            *class_id;
+    char            *uri;
+} pzPendingImfileRow;
+
+/** Creates a new pzPendingImfileRow object
+ *
+ *  @return A new pzPendingImfileRow object or NULL on failure.
+ */
+
+pzPendingImfileRow *pzPendingImfileRowAlloc(
+    const char      *exp_id,
+    psS32           bytes,
+    const char      *md5sum,
+    const char      *class,
+    const char      *class_id,
+    const char      *uri
+);
+
+/** Creates a new pzPendingImfile table
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a pzPendingImfile table
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    psS32           bytes,
+    const char      *md5sum,
+    const char      *class,
+    const char      *class_id,
+    const char      *uri
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfilePop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    psS32           *bytes,
+    char            **md5sum,
+    char            **class,
+    char            **class_id,
+    char            **uri
+);
+
+/** Insert a single pzPendingImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    pzPendingImfileRow *object             ///< pzPendingImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new pzPendingImfileRow on success or NULL on failure.
+ */
+
+pzPendingImfileRow *pzPendingImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table pzPendingImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool pzPendingImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a pzPendingImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *pzPendingImfileMetadataFromObject(
+    const pzPendingImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A pzPendingImfileRow pointer or NULL on error
+ */
+
+pzPendingImfileRow *pzPendingImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as pzPendingImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *pzPendingImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long pzPendingImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** newExpRow data structure
+ *
+ * Structure for representing a single row of newExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+} newExpRow;
+
+/** Creates a new newExpRow object
+ *
+ *  @return A new newExpRow object or NULL on failure.
+ */
+
+newExpRow *newExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles
+);
+
+/** Creates a new newExp table
+ *
+ * @return true on success
+ */
+
+bool newExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a newExp table
+ *
+ * @return true on success
+ */
+
+bool newExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool newExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool newExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles
+);
+
+/** Insert a single newExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool newExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    newExpRow       *object             ///< newExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new newExpRow on success or NULL on failure.
+ */
+
+newExpRow *newExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table newExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool newExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool newExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool newExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a newExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *newExpMetadataFromObject(
+    const newExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A newExpRow pointer or NULL on error
+ */
+
+newExpRow *newExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as newExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *newExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long newExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** newImfileRow data structure
+ *
+ * Structure for representing a single row of newImfile table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *class;
+    char            *class_id;
+    char            *uri;
+} newImfileRow;
+
+/** Creates a new newImfileRow object
+ *
+ *  @return A new newImfileRow object or NULL on failure.
+ */
+
+newImfileRow *newImfileRowAlloc(
+    const char      *exp_id,
+    const char      *class,
+    const char      *class_id,
+    const char      *uri
+);
+
+/** Creates a new newImfile table
+ *
+ * @return true on success
+ */
+
+bool newImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a newImfile table
+ *
+ * @return true on success
+ */
+
+bool newImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool newImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *class,
+    const char      *class_id,
+    const char      *uri
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool newImfilePop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **class,
+    char            **class_id,
+    char            **uri
+);
+
+/** Insert a single newImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool newImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    newImfileRow    *object             ///< newImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new newImfileRow on success or NULL on failure.
+ */
+
+newImfileRow *newImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table newImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool newImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool newImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool newImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a newImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *newImfileMetadataFromObject(
+    const newImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A newImfileRow pointer or NULL on error
+ */
+
+newImfileRow *newImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as newImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *newImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long newImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** rawDetrendExpRow data structure
+ *
+ * Structure for representing a single row of rawDetrendExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+} rawDetrendExpRow;
+
+/** Creates a new rawDetrendExpRow object
+ *
+ *  @return A new rawDetrendExpRow object or NULL on failure.
+ */
+
+rawDetrendExpRow *rawDetrendExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Creates a new rawDetrendExp table
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a rawDetrendExp table
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats
+);
+
+/** Insert a single rawDetrendExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    rawDetrendExpRow *object             ///< rawDetrendExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new rawDetrendExpRow on success or NULL on failure.
+ */
+
+rawDetrendExpRow *rawDetrendExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table rawDetrendExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool rawDetrendExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a rawDetrendExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *rawDetrendExpMetadataFromObject(
+    const rawDetrendExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A rawDetrendExpRow pointer or NULL on error
+ */
+
+rawDetrendExpRow *rawDetrendExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as rawDetrendExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *rawDetrendExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long rawDetrendExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** rawScienceExpRow data structure
+ *
+ * Structure for representing a single row of rawScienceExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+} rawScienceExpRow;
+
+/** Creates a new rawScienceExpRow object
+ *
+ *  @return A new rawScienceExpRow object or NULL on failure.
+ */
+
+rawScienceExpRow *rawScienceExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Creates a new rawScienceExp table
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a rawScienceExp table
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats
+);
+
+/** Insert a single rawScienceExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    rawScienceExpRow *object             ///< rawScienceExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new rawScienceExpRow on success or NULL on failure.
+ */
+
+rawScienceExpRow *rawScienceExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table rawScienceExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool rawScienceExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a rawScienceExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *rawScienceExpMetadataFromObject(
+    const rawScienceExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A rawScienceExpRow pointer or NULL on error
+ */
+
+rawScienceExpRow *rawScienceExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as rawScienceExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *rawScienceExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long rawScienceExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** rawImfileRow data structure
+ *
+ * Structure for representing a single row of rawImfile table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+} rawImfileRow;
+
+/** Creates a new rawImfileRow object
+ *
+ *  @return A new rawImfileRow object or NULL on failure.
+ */
+
+rawImfileRow *rawImfileRowAlloc(
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats
+);
+
+/** Creates a new rawImfile table
+ *
+ * @return true on success
+ */
+
+bool rawImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a rawImfile table
+ *
+ * @return true on success
+ */
+
+bool rawImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool rawImfilePop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **class_id,
+    char            **uri,
+    char            **stats
+);
+
+/** Insert a single rawImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool rawImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    rawImfileRow    *object             ///< rawImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new rawImfileRow on success or NULL on failure.
+ */
+
+rawImfileRow *rawImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table rawImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool rawImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool rawImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool rawImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a rawImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *rawImfileMetadataFromObject(
+    const rawImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A rawImfileRow pointer or NULL on error
+ */
+
+rawImfileRow *rawImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as rawImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *rawImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long rawImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p1PendingExpRow data structure
+ *
+ * Structure for representing a single row of p1PendingExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+} p1PendingExpRow;
+
+/** Creates a new p1PendingExpRow object
+ *
+ *  @return A new p1PendingExpRow object or NULL on failure.
+ */
+
+p1PendingExpRow *p1PendingExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version
+);
+
+/** Creates a new p1PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p1PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats,
+    char            **recipe,
+    psS32           *p1_version
+);
+
+/** Insert a single p1PendingExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p1PendingExpRow *object             ///< p1PendingExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p1PendingExpRow on success or NULL on failure.
+ */
+
+p1PendingExpRow *p1PendingExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p1PendingExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p1PendingExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p1PendingExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p1PendingExpMetadataFromObject(
+    const p1PendingExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p1PendingExpRow pointer or NULL on error
+ */
+
+p1PendingExpRow *p1PendingExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p1PendingExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p1PendingExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p1PendingExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p2PendingExpRow data structure
+ *
+ * Structure for representing a single row of p2PendingExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+} p2PendingExpRow;
+
+/** Creates a new p2PendingExpRow object
+ *
+ *  @return A new p2PendingExpRow object or NULL on failure.
+ */
+
+p2PendingExpRow *p2PendingExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Creates a new p2PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p2PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats,
+    char            **recipe,
+    psS32           *p1_version,
+    psS32           *p2_version
+);
+
+/** Insert a single p2PendingExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p2PendingExpRow *object             ///< p2PendingExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p2PendingExpRow on success or NULL on failure.
+ */
+
+p2PendingExpRow *p2PendingExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p2PendingExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p2PendingExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p2PendingExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p2PendingExpMetadataFromObject(
+    const p2PendingExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p2PendingExpRow pointer or NULL on error
+ */
+
+p2PendingExpRow *p2PendingExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p2PendingExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p2PendingExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p2PendingExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p2PendingImfileRow data structure
+ *
+ * Structure for representing a single row of p2PendingImfile table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+} p2PendingImfileRow;
+
+/** Creates a new p2PendingImfileRow object
+ *
+ *  @return A new p2PendingImfileRow object or NULL on failure.
+ */
+
+p2PendingImfileRow *p2PendingImfileRowAlloc(
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Creates a new p2PendingImfile table
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p2PendingImfile table
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfilePop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe,
+    psS32           *p1_version,
+    psS32           *p2_version
+);
+
+/** Insert a single p2PendingImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p2PendingImfileRow *object             ///< p2PendingImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p2PendingImfileRow on success or NULL on failure.
+ */
+
+p2PendingImfileRow *p2PendingImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p2PendingImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p2PendingImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p2PendingImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p2PendingImfileMetadataFromObject(
+    const p2PendingImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p2PendingImfileRow pointer or NULL on error
+ */
+
+p2PendingImfileRow *p2PendingImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p2PendingImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p2PendingImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p2PendingImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p2DoneExpRow data structure
+ *
+ * Structure for representing a single row of p2DoneExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+} p2DoneExpRow;
+
+/** Creates a new p2DoneExpRow object
+ *
+ *  @return A new p2DoneExpRow object or NULL on failure.
+ */
+
+p2DoneExpRow *p2DoneExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Creates a new p2DoneExp table
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p2DoneExp table
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats,
+    char            **recipe,
+    psS32           *p1_version,
+    psS32           *p2_version
+);
+
+/** Insert a single p2DoneExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p2DoneExpRow    *object             ///< p2DoneExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p2DoneExpRow on success or NULL on failure.
+ */
+
+p2DoneExpRow *p2DoneExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p2DoneExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p2DoneExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p2DoneExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p2DoneExpMetadataFromObject(
+    const p2DoneExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p2DoneExpRow pointer or NULL on error
+ */
+
+p2DoneExpRow *p2DoneExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p2DoneExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p2DoneExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p2DoneExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p2DoneImfileRow data structure
+ *
+ * Structure for representing a single row of p2DoneImfile table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+    psS32           p1_version;
+    psS32           p2_version;
+} p2DoneImfileRow;
+
+/** Creates a new p2DoneImfileRow object
+ *
+ *  @return A new p2DoneImfileRow object or NULL on failure.
+ */
+
+p2DoneImfileRow *p2DoneImfileRowAlloc(
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Creates a new p2DoneImfile table
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p2DoneImfile table
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p1_version,
+    psS32           p2_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfilePop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe,
+    psS32           *p1_version,
+    psS32           *p2_version
+);
+
+/** Insert a single p2DoneImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p2DoneImfileRow *object             ///< p2DoneImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p2DoneImfileRow on success or NULL on failure.
+ */
+
+p2DoneImfileRow *p2DoneImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p2DoneImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p2DoneImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p2DoneImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p2DoneImfileMetadataFromObject(
+    const p2DoneImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p2DoneImfileRow pointer or NULL on error
+ */
+
+p2DoneImfileRow *p2DoneImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p2DoneImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p2DoneImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p2DoneImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** p3PendingExpRow data structure
+ *
+ * Structure for representing a single row of p3PendingExp table data.
+ */
+
+typedef struct {
+    char            *exp_id;
+    char            *camera;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+    char            *recipe;
+    psS32           p2_version;
+    psS32           p3_version;
+} p3PendingExpRow;
+
+/** Creates a new p3PendingExpRow object
+ *
+ *  @return A new p3PendingExpRow object or NULL on failure.
+ */
+
+p3PendingExpRow *p3PendingExpRowAlloc(
+    const char      *exp_id,
+    const char      *camera,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p2_version,
+    psS32           p3_version
+);
+
+/** Creates a new p3PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a p3PendingExp table
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *exp_id,
+    const char      *camera,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats,
+    const char      *recipe,
+    psS32           p2_version,
+    psS32           p3_version
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpPop(
+    psDB            *dbh,               ///< Database handle
+    char            **exp_id,
+    char            **camera,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats,
+    char            **recipe,
+    psS32           *p2_version,
+    psS32           *p3_version
+);
+
+/** Insert a single p3PendingExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    p3PendingExpRow *object             ///< p3PendingExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new p3PendingExpRow on success or NULL on failure.
+ */
+
+p3PendingExpRow *p3PendingExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table p3PendingExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool p3PendingExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a p3PendingExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *p3PendingExpMetadataFromObject(
+    const p3PendingExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A p3PendingExpRow pointer or NULL on error
+ */
+
+p3PendingExpRow *p3PendingExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as p3PendingExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *p3PendingExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long p3PendingExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detRunRow data structure
+ *
+ * Structure for representing a single row of detRun table data.
+ */
+
+typedef struct {
+    char            *det_type;
+} detRunRow;
+
+/** Creates a new detRunRow object
+ *
+ *  @return A new detRunRow object or NULL on failure.
+ */
+
+detRunRow *detRunRowAlloc(
+    const char      *det_type
+);
+
+/** Creates a new detRun table
+ *
+ * @return true on success
+ */
+
+bool detRunCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detRun table
+ *
+ * @return true on success
+ */
+
+bool detRunDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detRunInsert(
+    psDB            *dbh,               ///< Database handle
+    const char      *det_type
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detRunPop(
+    psDB            *dbh,               ///< Database handle
+    char            **det_type
+);
+
+/** Insert a single detRunRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detRunInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detRunRow       *object             ///< detRunRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detRunRow on success or NULL on failure.
+ */
+
+detRunRow *detRunPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detRunRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detRunInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detRunPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detRunSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detRunRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detRunMetadataFromObject(
+    const detRunRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detRunRow pointer or NULL on error
+ */
+
+detRunRow *detRunObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detRunRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detRunSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detRunDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detInputExpRow data structure
+ *
+ * Structure for representing a single row of detInputExp table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *camera;
+    char            *telescope;
+    char            *exp_type;
+    psS32           imfiles;
+    char            *filter;
+    char            *stats;
+} detInputExpRow;
+
+/** Creates a new detInputExpRow object
+ *
+ *  @return A new detInputExpRow object or NULL on failure.
+ */
+
+detInputExpRow *detInputExpRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Creates a new detInputExp table
+ *
+ * @return true on success
+ */
+
+bool detInputExpCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detInputExp table
+ *
+ * @return true on success
+ */
+
+bool detInputExpDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detInputExpInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *camera,
+    const char      *telescope,
+    const char      *exp_type,
+    psS32           imfiles,
+    const char      *filter,
+    const char      *stats
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detInputExpPop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **exp_id,
+    char            **camera,
+    char            **telescope,
+    char            **exp_type,
+    psS32           *imfiles,
+    char            **filter,
+    char            **stats
+);
+
+/** Insert a single detInputExpRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detInputExpInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detInputExpRow  *object             ///< detInputExpRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detInputExpRow on success or NULL on failure.
+ */
+
+detInputExpRow *detInputExpPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detInputExpRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detInputExpInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detInputExpPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detInputExpSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detInputExpRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detInputExpMetadataFromObject(
+    const detInputExpRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detInputExpRow pointer or NULL on error
+ */
+
+detInputExpRow *detInputExpObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detInputExpRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detInputExpSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detInputExpDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detProcessedImfileRow data structure
+ *
+ * Structure for representing a single row of detProcessedImfile table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    char            *exp_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+} detProcessedImfileRow;
+
+/** Creates a new detProcessedImfileRow object
+ *
+ *  @return A new detProcessedImfileRow object or NULL on failure.
+ */
+
+detProcessedImfileRow *detProcessedImfileRowAlloc(
+    psS32           det_id,
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Creates a new detProcessedImfile table
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detProcessedImfile table
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfilePop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    char            **exp_id,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe
+);
+
+/** Insert a single detProcessedImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detProcessedImfileRow *object             ///< detProcessedImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detProcessedImfileRow on success or NULL on failure.
+ */
+
+detProcessedImfileRow *detProcessedImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detProcessedImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detProcessedImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detProcessedImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detProcessedImfileMetadataFromObject(
+    const detProcessedImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detProcessedImfileRow pointer or NULL on error
+ */
+
+detProcessedImfileRow *detProcessedImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detProcessedImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detProcessedImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detProcessedImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detStackedImfileRow data structure
+ *
+ * Structure for representing a single row of detStackedImfile table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+} detStackedImfileRow;
+
+/** Creates a new detStackedImfileRow object
+ *
+ *  @return A new detStackedImfileRow object or NULL on failure.
+ */
+
+detStackedImfileRow *detStackedImfileRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Creates a new detStackedImfile table
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detStackedImfile table
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detStackedImfilePop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe
+);
+
+/** Insert a single detStackedImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detStackedImfileRow *object             ///< detStackedImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detStackedImfileRow on success or NULL on failure.
+ */
+
+detStackedImfileRow *detStackedImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detStackedImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detStackedImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detStackedImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detStackedImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detStackedImfileMetadataFromObject(
+    const detStackedImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detStackedImfileRow pointer or NULL on error
+ */
+
+detStackedImfileRow *detStackedImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detStackedImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detStackedImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detStackedImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detNormalizedImfileRow data structure
+ *
+ * Structure for representing a single row of detNormalizedImfile table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+} detNormalizedImfileRow;
+
+/** Creates a new detNormalizedImfileRow object
+ *
+ *  @return A new detNormalizedImfileRow object or NULL on failure.
+ */
+
+detNormalizedImfileRow *detNormalizedImfileRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Creates a new detNormalizedImfile table
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detNormalizedImfile table
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfilePop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe
+);
+
+/** Insert a single detNormalizedImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detNormalizedImfileRow *object             ///< detNormalizedImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detNormalizedImfileRow on success or NULL on failure.
+ */
+
+detNormalizedImfileRow *detNormalizedImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detNormalizedImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detNormalizedImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detNormalizedImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detNormalizedImfileMetadataFromObject(
+    const detNormalizedImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detNormalizedImfileRow pointer or NULL on error
+ */
+
+detNormalizedImfileRow *detNormalizedImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detNormalizedImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detNormalizedImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detNormalizedImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detMasterFrameRow data structure
+ *
+ * Structure for representing a single row of detMasterFrame table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *comment;
+} detMasterFrameRow;
+
+/** Creates a new detMasterFrameRow object
+ *
+ *  @return A new detMasterFrameRow object or NULL on failure.
+ */
+
+detMasterFrameRow *detMasterFrameRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *comment
+);
+
+/** Creates a new detMasterFrame table
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detMasterFrame table
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *comment
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detMasterFramePop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **comment
+);
+
+/** Insert a single detMasterFrameRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detMasterFrameRow *object             ///< detMasterFrameRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detMasterFrameRow on success or NULL on failure.
+ */
+
+detMasterFrameRow *detMasterFramePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detMasterFrameRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detMasterFramePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detMasterFrameSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detMasterFrameRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detMasterFrameMetadataFromObject(
+    const detMasterFrameRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detMasterFrameRow pointer or NULL on error
+ */
+
+detMasterFrameRow *detMasterFrameObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detMasterFrameRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detMasterFrameSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detMasterFrameDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detMasterImfileRow data structure
+ *
+ * Structure for representing a single row of detMasterImfile table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    char            *class_id;
+    char            *uri;
+    char            *stats;
+    char            *recipe;
+} detMasterImfileRow;
+
+/** Creates a new detMasterImfileRow object
+ *
+ *  @return A new detMasterImfileRow object or NULL on failure.
+ */
+
+detMasterImfileRow *detMasterImfileRowAlloc(
+    psS32           det_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Creates a new detMasterImfile table
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detMasterImfile table
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    const char      *class_id,
+    const char      *uri,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detMasterImfilePop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    char            **class_id,
+    char            **uri,
+    char            **stats,
+    char            **recipe
+);
+
+/** Insert a single detMasterImfileRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detMasterImfileRow *object             ///< detMasterImfileRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detMasterImfileRow on success or NULL on failure.
+ */
+
+detMasterImfileRow *detMasterImfilePopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detMasterImfileRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detMasterImfilePopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detMasterImfileSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detMasterImfileRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detMasterImfileMetadataFromObject(
+    const detMasterImfileRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detMasterImfileRow pointer or NULL on error
+ */
+
+detMasterImfileRow *detMasterImfileObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detMasterImfileRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detMasterImfileSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detMasterImfileDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detResidImfileAnalysisRow data structure
+ *
+ * Structure for representing a single row of detResidImfileAnalysis table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *class_id;
+    char            *stats;
+    char            *recipe;
+} detResidImfileAnalysisRow;
+
+/** Creates a new detResidImfileAnalysisRow object
+ *
+ *  @return A new detResidImfileAnalysisRow object or NULL on failure.
+ */
+
+detResidImfileAnalysisRow *detResidImfileAnalysisRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Creates a new detResidImfileAnalysis table
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detResidImfileAnalysis table
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *class_id,
+    const char      *stats,
+    const char      *recipe
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisPop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **exp_id,
+    char            **class_id,
+    char            **stats,
+    char            **recipe
+);
+
+/** Insert a single detResidImfileAnalysisRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detResidImfileAnalysisRow *object             ///< detResidImfileAnalysisRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detResidImfileAnalysisRow on success or NULL on failure.
+ */
+
+detResidImfileAnalysisRow *detResidImfileAnalysisPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detResidImfileAnalysisRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detResidImfileAnalysisSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detResidImfileAnalysisRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detResidImfileAnalysisMetadataFromObject(
+    const detResidImfileAnalysisRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detResidImfileAnalysisRow pointer or NULL on error
+ */
+
+detResidImfileAnalysisRow *detResidImfileAnalysisObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detResidImfileAnalysisRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detResidImfileAnalysisSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detResidImfileAnalysisDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+/** detResidExpAnalysisRow data structure
+ *
+ * Structure for representing a single row of detResidExpAnalysis table data.
+ */
+
+typedef struct {
+    psS32           det_id;
+    psS32           iteration;
+    char            *exp_id;
+    char            *stats;
+    char            *recipe;
+    bool            accept;
+} detResidExpAnalysisRow;
+
+/** Creates a new detResidExpAnalysisRow object
+ *
+ *  @return A new detResidExpAnalysisRow object or NULL on failure.
+ */
+
+detResidExpAnalysisRow *detResidExpAnalysisRowAlloc(
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *stats,
+    const char      *recipe,
+    bool            accept
+);
+
+/** Creates a new detResidExpAnalysis table
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisCreateTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Deletes a detResidExpAnalysis table
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisDropTable(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert a single row into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisInsert(
+    psDB            *dbh,               ///< Database handle
+    psS32           det_id,
+    psS32           iteration,
+    const char      *exp_id,
+    const char      *stats,
+    const char      *recipe,
+    bool            accept
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisPop(
+    psDB            *dbh,               ///< Database handle
+    psS32           *det_id,
+    psS32           *iteration,
+    char            **exp_id,
+    char            **stats,
+    char            **recipe,
+    bool            *accept
+);
+
+/** Insert a single detResidExpAnalysisRow object into a table
+ *
+ * This function constructs and inserts a single row based on it's parameters.
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisInsertObject(
+    psDB            *dbh,               ///< Database handle
+    detResidExpAnalysisRow *object             ///< detResidExpAnalysisRow object
+);
+
+/** Removes the last row from the database and returns it
+ *
+ * @return A new detResidExpAnalysisRow on success or NULL on failure.
+ */
+
+detResidExpAnalysisRow *detResidExpAnalysisPopObject(
+    psDB            *dbh                ///< Database handle
+);
+
+/** Insert data from a binary FITS table detResidExpAnalysisRow into the database
+ *
+ * This function expects a psFits object with a FITS table as the first
+ * extension.  The table must have at least one row of data in it, that is of
+ * the appropriate format (number of columns and their type).  All other
+ * extensions are ignored.
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisInsertFits(
+    psDB            *dbh,               ///< Database handle
+    const psFits    *fits               ///< psFits object
+);
+
+/** Removes the last limit row from the database and returns them in a binary FITS table.
+ *
+ * This function assumes an empty psFits object and will create a FITS table as
+ * the first extension. 
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisPopFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Selects up to limit from the database and returns them in a binary FITS table
+ *
+ * This function assumes an empty psFits object and will create a FITS table
+ * as the first extension.
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return true on success
+ */
+
+bool detResidExpAnalysisSelectRowsFits(
+    psDB            *dbh,               ///< Database handle
+    psFits          *fits,              ///< psFits object
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+
+/** Convert a detResidExpAnalysisRow into an equivalent psMetadata
+ *
+ * @return A psMetadata pointer or NULL on error
+ */
+
+psMetadata *detResidExpAnalysisMetadataFromObject(
+    const detResidExpAnalysisRow *object             ///< fooRow to convert into a psMetadata
+);
+
+/** Convert a psMetadata into an equivalent fooRow
+ *
+ * @return A detResidExpAnalysisRow pointer or NULL on error
+ */
+
+detResidExpAnalysisRow *detResidExpAnalysisObjectFromMetadata(
+    psMetadata      *md                 ///< psMetadata to convert into a fooRow
+);
+/** Selects up to limit rows from the database and returns as detResidExpAnalysisRow objects in a psArray
+ *
+ *  See psDBSelectRows() for documentation on the format of where.
+ *
+ * @return A psArray pointer or NULL on error
+ */
+
+psArray *detResidExpAnalysisSelectRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psMetadata *where,            ///< Row match criteria
+    unsigned long long limit            ///< Maximum number of elements to return
+);
+/** Deletes up to limit rows from the database and returns the number of rows actually deleted. 
+ *
+ *  Note that a 'where' search psMetadata is constructed from each object and
+ *  used to find rows to delete.
+ *
+ * @return A The number of rows removed or a negative value on error
+ */
+
+long long detResidExpAnalysisDeleteRowObjects(
+    psDB            *dbh,               ///< Database handle
+    const psArray   *objects,           ///< Array of objects to delete
+    unsigned long long limit            ///< Maximum number of elements to delete 
+);
+
+/// @}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // DETRESIDEXPANALYSIS_DB_H
Index: /tags/rel-0_0_1/ippdb/tests/Makefile.am
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/Makefile.am	(revision 7462)
@@ -0,0 +1,67 @@
+# copied from bison-1.875d
+
+EXTRA_DIST = $(TESTSUITE_AT) testsuite package.m4
+
+DISTCLEANFILES       = atconfig $(check_SCRIPTS)
+MAINTAINERCLEANFILES = Makefile.in $(TESTSUITE)
+
+## ------------ ##
+## package.m4.  ##
+## ------------ ##
+
+$(srcdir)/package.m4: $(top_srcdir)/configure.ac
+	{					\
+	  echo '# Signature of the current package.'; \
+	  echo 'm4_define([AT_PACKAGE_NAME],      [@PACKAGE_NAME@])'; \
+	  echo 'm4_define([AT_PACKAGE_TARNAME],   [@PACKAGE_TARNAME@])'; \
+	  echo 'm4_define([AT_PACKAGE_VERSION],   [@PACKAGE_VERSION@])'; \
+	  echo 'm4_define([AT_PACKAGE_STRING],    [@PACKAGE_STRING@])'; \
+	  echo 'm4_define([AT_PACKAGE_BUGREPORT], [@PACKAGE_BUGREPORT@])'; \
+	} >$(srcdir)/package.m4
+
+## ------------ ##
+## Test suite.  ##
+## ------------ ##
+
+TESTSUITE = $(srcdir)/testsuite
+
+AUTOTEST = $(AUTOM4TE) --language=autotest
+$(TESTSUITE): package.m4 $(TESTSUITE_AT)
+	$(AUTOTEST) -I $(srcdir) testsuite.at -o $@.tmp
+	mv $@.tmp $@
+
+atconfig: $(top_builddir)/config.status
+	cd $(top_builddir) && ./config.status tests/$@
+
+clean-local:
+	test ! -f $(TESTSUITE) || $(SHELL) $(TESTSUITE) --clean
+
+check-local: atconfig $(TESTSUITE)
+	$(SHELL) $(TESTSUITE)
+
+#
+# END BISON
+#
+
+UNITS = \
+	alloc \
+	init \
+	cleanup \
+	createtable \
+    droptable \
+    insert \
+    pop \
+    insertobject \
+    popobject \
+    insertfits \
+    popfits \
+    selectrowsfits \
+    metadatafromobject \
+    objectfrommetadata
+
+AM_CPPFLAGS = -I$(top_srcdir)/src$
+AM_CFLAGS   = @ippdb_CFLAGS@ $(PSLIB_CFLAGS)$
+AM_LDFLAGS  = $(PSLIB_LIBS)$
+LDADD       = $(top_builddir)/src/libippdb.la$
+
+check_PROGRAMS = dbsetup dbcleanup $(UNITS)
Index: /tags/rel-0_0_1/ippdb/tests/alloc.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/alloc.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/alloc.c	(revision 7462)
@@ -0,0 +1,1189 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+#define MAX_STRING_LENGTH 1024
+
+int main ()
+{
+    {
+        weatherRow      *object;
+
+        object = weatherRowAlloc(32.32, 32.32, 32.32, 32.32, 32.32, 32.32, 32.32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->temp01 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi01 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->temp02 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi02 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->temp03 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi03 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->pressure == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        skyp_transparencyRow *object;
+
+        object = skyp_transparencyRowAlloc("a string", 64.64, -32, 64.64, 64.64, 32.32, 64.64    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->trans == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->nstars == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        skyp_absorptionRow *object;
+
+        object = skyp_absorptionRowAlloc("a string", 32.32, 32.32, 32.32, -32, 64.64, 64.64, 32.32, 64.64    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->disperser_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp1 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp2 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp3 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->nstars == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        skyp_emissionRow *object;
+
+        object = skyp_emissionRowAlloc("a string", 32.32, 32.32, 32.32, 32.32, 32.32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->disperser_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp1 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp2 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp3 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->continuum == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        dimmRow         *object;
+
+        object = dimmRowAlloc(32.32, 32.32, 32.32, 64.64, 64.64, 32.32, "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->sigmax == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sigmay == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fwhm == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->expttime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        skyp_irRow      *object;
+
+        object = skyp_irRowAlloc(64.64, 64.64, 64.64, 64.64, 32.32, 32.32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_var == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fov_x == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fov_y == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        domeRow         *object;
+
+        object = domeRowAlloc(32.32, true, true, true    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->az == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->open == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->light == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->track == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        telescopeRow    *object;
+
+        object = telescopeRowAlloc("a string", 32.32, 32.32, 64.64, 64.64    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->guide, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->alt == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->az == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        summitExpRow    *object;
+
+        object = summitExpRowAlloc("a string", "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        pzPendingExpRow *object;
+
+        object = pzPendingExpRowAlloc("a string", "a string", "a string", "a string", -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        pzPendingImfileRow *object;
+
+        object = pzPendingImfileRowAlloc("a string", -32, "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->bytes == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->md5sum, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        newExpRow       *object;
+
+        object = newExpRowAlloc("a string", "a string", "a string", "a string", -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        newImfileRow    *object;
+
+        object = newImfileRowAlloc("a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        rawDetrendExpRow *object;
+
+        object = rawDetrendExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        rawScienceExpRow *object;
+
+        object = rawScienceExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        rawImfileRow    *object;
+
+        object = rawImfileRowAlloc("a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p1PendingExpRow *object;
+
+        object = p1PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p2PendingExpRow *object;
+
+        object = p2PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p2PendingImfileRow *object;
+
+        object = p2PendingImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p2DoneExpRow    *object;
+
+        object = p2DoneExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p2DoneImfileRow *object;
+
+        object = p2DoneImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        p3PendingExpRow *object;
+
+        object = p3PendingExpRowAlloc("a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p3_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detRunRow       *object;
+
+        object = detRunRowAlloc("a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (strncmp(object->det_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detInputExpRow  *object;
+
+        object = detInputExpRowAlloc(-32, -32, "a string", "a string", "a string", "a string", -32, "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detProcessedImfileRow *object;
+
+        object = detProcessedImfileRowAlloc(-32, "a string", "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detStackedImfileRow *object;
+
+        object = detStackedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detNormalizedImfileRow *object;
+
+        object = detNormalizedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detMasterFrameRow *object;
+
+        object = detMasterFrameRowAlloc(-32, -32, "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->comment, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detMasterImfileRow *object;
+
+        object = detMasterImfileRowAlloc(-32, "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detResidImfileAnalysisRow *object;
+
+        object = detResidImfileAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", "a string"    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        detResidExpAnalysisRow *object;
+
+        object = detResidExpAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", true    );
+
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->accept == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/cleanup.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/cleanup.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/cleanup.c	(revision 7462)
@@ -0,0 +1,17 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    psDB            *dbh;
+
+    dbh = psDBInit("localhost", "test", NULL, "test");
+    if (!dbh) {
+        exit(EXIT_FAILURE);
+    }
+
+    ippdbCleanup(dbh);
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/createtable.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/createtable.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/createtable.c	(revision 7462)
@@ -0,0 +1,473 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!weatherCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!skyp_transparencyCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!skyp_absorptionCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!skyp_emissionCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!dimmCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!skyp_irCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!domeCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!telescopeCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!summitExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!pzPendingExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!pzPendingImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!newExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!newImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!rawDetrendExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!rawScienceExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!rawImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p1PendingExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p2PendingExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p2PendingImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p2DoneExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p2DoneImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!p3PendingExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detRunCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detInputExpCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detProcessedImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detStackedImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detNormalizedImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detMasterFrameCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detMasterImfileCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detResidImfileAnalysisCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if(!detResidExpAnalysisCreateTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/dbcleanup.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/dbcleanup.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/dbcleanup.c	(revision 7462)
@@ -0,0 +1,48 @@
+#include <pslib.h>
+#include <stdlib.h>
+
+int main ()
+{
+    psDB            *dbh;
+
+    dbh = psDBInit("localhost", "test", NULL, "test");
+    if (!dbh) {
+        exit(EXIT_FAILURE);
+    }
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS weather");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_transparency");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_absorption");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_emission");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS dimm");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_ir");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS dome");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS telescope");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS summitExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS pzPendingExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS pzPendingImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS newExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS newImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawDetrendExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawScienceExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p1PendingExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2PendingExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2PendingImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2DoneExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2DoneImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p3PendingExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detRun");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detInputExp");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detProcessedImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detStackedImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detNormalizedImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detMasterFrame");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detMasterImfile");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detResidImfileAnalysis");
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detResidExpAnalysis");
+
+    psDBCleanup(dbh);
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/dbsetup.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/dbsetup.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/dbsetup.c	(revision 7462)
@@ -0,0 +1,111 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    psDB            *dbh;
+
+    dbh = psDBInit("localhost", "test", NULL, "test");
+    if (!dbh) {
+        exit(EXIT_FAILURE);
+    }
+
+    // remove the table if it already exists
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS weather");
+    weatherCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_transparency");
+    skyp_transparencyCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_absorption");
+    skyp_absorptionCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_emission");
+    skyp_emissionCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS dimm");
+    dimmCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS skyp_ir");
+    skyp_irCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS dome");
+    domeCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS telescope");
+    telescopeCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS summitExp");
+    summitExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS pzPendingExp");
+    pzPendingExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS pzPendingImfile");
+    pzPendingImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS newExp");
+    newExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS newImfile");
+    newImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawDetrendExp");
+    rawDetrendExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawScienceExp");
+    rawScienceExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS rawImfile");
+    rawImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p1PendingExp");
+    p1PendingExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2PendingExp");
+    p2PendingExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2PendingImfile");
+    p2PendingImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2DoneExp");
+    p2DoneExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p2DoneImfile");
+    p2DoneImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS p3PendingExp");
+    p3PendingExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detRun");
+    detRunCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detInputExp");
+    detInputExpCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detProcessedImfile");
+    detProcessedImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detStackedImfile");
+    detStackedImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detNormalizedImfile");
+    detNormalizedImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detMasterFrame");
+    detMasterFrameCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detMasterImfile");
+    detMasterImfileCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detResidImfileAnalysis");
+    detResidImfileAnalysisCreateTable(dbh);
+
+    p_psDBRunQuery(dbh, "DROP TABLE IF EXISTS detResidExpAnalysis");
+    detResidExpAnalysisCreateTable(dbh);
+
+    psDBCleanup(dbh);
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/droptable.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/droptable.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/droptable.c	(revision 7462)
@@ -0,0 +1,473 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domeDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopeDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFrameDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfileDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisDropTable(dbh)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/init.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/init.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/init.c	(revision 7462)
@@ -0,0 +1,17 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    psDB            *dbh;
+
+    dbh = ippdbInit("localhost", "test", NULL, "test");
+    if (!dbh) {
+        exit(EXIT_FAILURE);
+    }
+
+    psDBCleanup(dbh);
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/insert.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/insert.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/insert.c	(revision 7462)
@@ -0,0 +1,473 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherInsert(dbh, 32.32, 32.32, 32.32, 32.32, 32.32, 32.32, 32.32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyInsert(dbh, "a string", 64.64, -32, 64.64, 64.64, 32.32, 64.64)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionInsert(dbh, "a string", 32.32, 32.32, 32.32, -32, 64.64, 64.64, 32.32, 64.64)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionInsert(dbh, "a string", 32.32, 32.32, 32.32, 32.32, 32.32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmInsert(dbh, 32.32, 32.32, 32.32, 64.64, 64.64, 32.32, "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irInsert(dbh, 64.64, 64.64, 64.64, 64.64, 32.32, 32.32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domeInsert(dbh, 32.32, true, true, true)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopeInsert(dbh, "a string", 32.32, 32.32, 64.64, 64.64)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpInsert(dbh, "a string", "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpInsert(dbh, "a string", "a string", "a string", "a string", -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfileInsert(dbh, "a string", -32, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpInsert(dbh, "a string", "a string", "a string", "a string", -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfileInsert(dbh, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpInsert(dbh, "a string", "a string", "a string", "a string", -32, "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpInsert(dbh, "a string", "a string", "a string", "a string", -32, "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfileInsert(dbh, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpInsert(dbh, "a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpInsert(dbh, "a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfileInsert(dbh, "a string", "a string", "a string", "a string", "a string", -32, -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpInsert(dbh, "a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfileInsert(dbh, "a string", "a string", "a string", "a string", "a string", -32, -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpInsert(dbh, "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunInsert(dbh, "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpInsert(dbh, -32, -32, "a string", "a string", "a string", "a string", -32, "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfileInsert(dbh, -32, "a string", "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfileInsert(dbh, -32, -32, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfileInsert(dbh, -32, -32, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFrameInsert(dbh, -32, -32, "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfileInsert(dbh, -32, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisInsert(dbh, -32, -32, "a string", "a string", "a string", "a string")) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisInsert(dbh, -32, -32, "a string", "a string", "a string", true)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/insertfits.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/insertfits.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/insertfits.c	(revision 7462)
@@ -0,0 +1,818 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define TMP_FILENAME "./blargh"
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domeInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopeInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFrameInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfileInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // open a temp
+        fits = psFitsOpen(TMP_FILENAME, "r");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisInsertFits(dbh, fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/insertobject.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/insertobject.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/insertobject.c	(revision 7462)
@@ -0,0 +1,690 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        weatherRow      *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = weatherRowAlloc(32.32, 32.32, 32.32, 32.32, 32.32, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_transparencyRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_transparencyRowAlloc("a string", 64.64, -32, 64.64, 64.64, 32.32, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_absorptionRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_absorptionRowAlloc("a string", 32.32, 32.32, 32.32, -32, 64.64, 64.64, 32.32, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_emissionRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_emissionRowAlloc("a string", 32.32, 32.32, 32.32, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        dimmRow         *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = dimmRowAlloc(32.32, 32.32, 32.32, 64.64, 64.64, 32.32, "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_irRow      *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_irRowAlloc(64.64, 64.64, 64.64, 64.64, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        domeRow         *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = domeRowAlloc(32.32, true, true, true);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domeInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        telescopeRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = telescopeRowAlloc("a string", 32.32, 32.32, 64.64, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopeInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        summitExpRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = summitExpRowAlloc("a string", "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        pzPendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingExpRowAlloc("a string", "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        pzPendingImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingImfileRowAlloc("a string", -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        newExpRow       *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = newExpRowAlloc("a string", "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        newImfileRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = newImfileRowAlloc("a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawDetrendExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawDetrendExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawScienceExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawScienceExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawImfileRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawImfileRowAlloc("a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p1PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p1PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2PendingImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2DoneExpRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2DoneImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p3PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p3PendingExpRowAlloc("a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detRunRow       *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detRunRowAlloc("a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detInputExpRow  *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detInputExpRowAlloc(-32, -32, "a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detProcessedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detProcessedImfileRowAlloc(-32, "a string", "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detStackedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detStackedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detNormalizedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detNormalizedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detMasterFrameRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterFrameRowAlloc(-32, -32, "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFrameInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detMasterImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterImfileRowAlloc(-32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfileInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detResidImfileAnalysisRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidImfileAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detResidExpAnalysisRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidExpAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", true);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisInsertObject(dbh, object)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/metadatafromobject.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/metadatafromobject.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/metadatafromobject.c	(revision 7462)
@@ -0,0 +1,1438 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_STRING_LENGTH 1024
+
+int main ()
+{
+    {
+        psMetadata      *md;
+        weatherRow      *object;
+        bool            status;
+
+        object = weatherRowAlloc(32.32, 32.32, 32.32, 32.32, 32.32, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = weatherMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupF32(&status, md, "temp01") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "humi01") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "temp02") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "humi02") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "temp03") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "humi03") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "pressure") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_transparencyRow *object;
+        bool            status;
+
+        object = skyp_transparencyRowAlloc("a string", 64.64, -32, 64.64, 64.64, 32.32, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = skyp_transparencyMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "trans") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "nstars") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "ra") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "decl") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "exptime") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "sky_bright") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_absorptionRow *object;
+        bool            status;
+
+        object = skyp_absorptionRowAlloc("a string", 32.32, 32.32, 32.32, -32, 64.64, 64.64, 32.32, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = skyp_absorptionMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "disperser_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp1") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp2") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp3") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "nstars") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "ra") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "decl") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "exptime") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "sky_bright") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_emissionRow *object;
+        bool            status;
+
+        object = skyp_emissionRowAlloc("a string", 32.32, 32.32, 32.32, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = skyp_emissionMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "disperser_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp1") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp2") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "atmcomp3") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "continuum") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "exptime") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        dimmRow         *object;
+        bool            status;
+
+        object = dimmRowAlloc(32.32, 32.32, 32.32, 64.64, 64.64, 32.32, "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = dimmMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupF32(&status, md, "sigmax") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "sigmay") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "fwhm") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "ra") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "decl") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "expttime") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_irRow      *object;
+        bool            status;
+
+        object = skyp_irRowAlloc(64.64, 64.64, 64.64, 64.64, 32.32, 32.32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = skyp_irMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupF64(&status, md, "sky_bright") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "sky_var") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "ra") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "decl") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "fov_x") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "fov_y") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        domeRow         *object;
+        bool            status;
+
+        object = domeRowAlloc(32.32, true, true, true);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = domeMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupF32(&status, md, "az") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupBool(&status, md, "open") == true) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupBool(&status, md, "light") == true) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupBool(&status, md, "track") == true) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        telescopeRow    *object;
+        bool            status;
+
+        object = telescopeRowAlloc("a string", 32.32, 32.32, 64.64, 64.64);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = telescopeMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "guide"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "alt") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF32(&status, md, "az") == 32.32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "ra") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupF64(&status, md, "decl") == 64.64) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        summitExpRow    *object;
+        bool            status;
+
+        object = summitExpRowAlloc("a string", "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = summitExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        pzPendingExpRow *object;
+        bool            status;
+
+        object = pzPendingExpRowAlloc("a string", "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = pzPendingExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        pzPendingImfileRow *object;
+        bool            status;
+
+        object = pzPendingImfileRowAlloc("a string", -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = pzPendingImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "bytes") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "md5sum"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        newExpRow       *object;
+        bool            status;
+
+        object = newExpRowAlloc("a string", "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = newExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        newImfileRow    *object;
+        bool            status;
+
+        object = newImfileRowAlloc("a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = newImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        rawDetrendExpRow *object;
+        bool            status;
+
+        object = rawDetrendExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = rawDetrendExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        rawScienceExpRow *object;
+        bool            status;
+
+        object = rawScienceExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = rawScienceExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        rawImfileRow    *object;
+        bool            status;
+
+        object = rawImfileRowAlloc("a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = rawImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p1PendingExpRow *object;
+        bool            status;
+
+        object = p1PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p1PendingExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p1_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p2PendingExpRow *object;
+        bool            status;
+
+        object = p2PendingExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p2PendingExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p1_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p2_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p2PendingImfileRow *object;
+        bool            status;
+
+        object = p2PendingImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p2PendingImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p1_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p2_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p2DoneExpRow    *object;
+        bool            status;
+
+        object = p2DoneExpRowAlloc("a string", "a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p2DoneExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p1_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p2_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p2DoneImfileRow *object;
+        bool            status;
+
+        object = p2DoneImfileRowAlloc("a string", "a string", "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p2DoneImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p1_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p2_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        p3PendingExpRow *object;
+        bool            status;
+
+        object = p3PendingExpRowAlloc("a string", "a string", "a string", -32, "a string", "a string", "a string", -32, -32);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = p3PendingExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p2_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "p3_version") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detRunRow       *object;
+        bool            status;
+
+        object = detRunRowAlloc("a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detRunMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (strncmp(psMetadataLookupPtr(&status, md, "det_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detInputExpRow  *object;
+        bool            status;
+
+        object = detInputExpRowAlloc(-32, -32, "a string", "a string", "a string", "a string", -32, "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detInputExpMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "camera"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "telescope"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_type"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "imfiles") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "filter"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detProcessedImfileRow *object;
+        bool            status;
+
+        object = detProcessedImfileRowAlloc(-32, "a string", "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detProcessedImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detStackedImfileRow *object;
+        bool            status;
+
+        object = detStackedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detStackedImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detNormalizedImfileRow *object;
+        bool            status;
+
+        object = detNormalizedImfileRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detNormalizedImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detMasterFrameRow *object;
+        bool            status;
+
+        object = detMasterFrameRowAlloc(-32, -32, "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detMasterFrameMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "comment"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detMasterImfileRow *object;
+        bool            status;
+
+        object = detMasterImfileRowAlloc(-32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detMasterImfileMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "uri"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detResidImfileAnalysisRow *object;
+        bool            status;
+
+        object = detResidImfileAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", "a string");
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detResidImfileAnalysisMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "class_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    {
+        psMetadata      *md;
+        detResidExpAnalysisRow *object;
+        bool            status;
+
+        object = detResidExpAnalysisRowAlloc(-32, -32, "a string", "a string", "a string", true);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        md = detResidExpAnalysisMetadataFromObject(object);
+        if (!md) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+
+        if (!psMetadataLookupS32(&status, md, "det_id") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupS32(&status, md, "iteration") == -32) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "exp_id"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "stats"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(psMetadataLookupPtr(&status, md, "recipe"), "a string", MAX_STRING_LENGTH)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataLookupBool(&status, md, "accept") == true) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/objectfrommetadata.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/objectfrommetadata.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/objectfrommetadata.c	(revision 7462)
@@ -0,0 +1,2121 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define MAX_STRING_LENGTH 1024
+
+int main ()
+{
+    {
+        psMetadata      *md;
+        weatherRow      *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp01", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi01", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp02", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi02", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "temp03", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "humi03", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "pressure", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = weatherObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->temp01 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi01 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->temp02 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi02 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->temp03 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->humi03 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->pressure == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_transparencyRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "trans", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_transparencyObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->trans == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->nstars == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_absorptionRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "nstars", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_absorptionObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->disperser_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp1 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp2 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp3 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->nstars == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_emissionRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "disperser_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp1", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp2", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "atmcomp3", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "continuum", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "exptime", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_emissionObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->disperser_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp1 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp2 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->atmcomp3 == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->continuum == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->exptime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        dimmRow         *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmax", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "sigmay", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "fwhm", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "expttime", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = dimmObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->sigmax == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sigmay == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fwhm == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->expttime == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        skyp_irRow      *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_bright", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "sky_var", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_x", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "fov_y", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_irObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->sky_bright == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->sky_var == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fov_x == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->fov_y == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        domeRow         *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAdd(md, PS_LIST_TAIL, "open", PS_DATA_BOOL, NULL, true)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAdd(md, PS_LIST_TAIL, "light", PS_DATA_BOOL, NULL, true)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAdd(md, PS_LIST_TAIL, "track", PS_DATA_BOOL, NULL, true)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = domeObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->az == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->open == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->light == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->track == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        telescopeRow    *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "guide", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "alt", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF32(md, PS_LIST_TAIL, "az", 0, NULL, 32.32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "ra", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddF64(md, PS_LIST_TAIL, "decl", 0, NULL, 64.64)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = telescopeObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->guide, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->alt == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->az == 32.32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->ra == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->decl == 64.64) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        summitExpRow    *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = summitExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        pzPendingExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        pzPendingImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "bytes", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "md5sum", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->bytes == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->md5sum, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        newExpRow       *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = newExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        newImfileRow    *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = newImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        rawDetrendExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawDetrendExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        rawScienceExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawScienceExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        rawImfileRow    *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p1PendingExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p1PendingExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p2PendingExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p2PendingImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p2DoneExpRow    *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p2DoneImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p1_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p1_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        p3PendingExpRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p2_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "p3_version", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = p3PendingExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p2_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->p3_version == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detRunRow       *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "det_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detRunObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (strncmp(object->det_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detInputExpRow  *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "camera", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "telescope", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_type", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "imfiles", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "filter", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detInputExpObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->camera, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->telescope, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_type, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->imfiles == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->filter, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detProcessedImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detProcessedImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detStackedImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detStackedImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detNormalizedImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detNormalizedImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detMasterFrameRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "comment", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterFrameObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->comment, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detMasterImfileRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "uri", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterImfileObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->uri, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detResidImfileAnalysisRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "class_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidImfileAnalysisObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->class_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    {
+        psMetadata      *md;
+        detResidExpAnalysisRow *object;
+
+        md = psMetadataAlloc();
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "det_id", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddS32(md, PS_LIST_TAIL, "iteration", 0, NULL, -32)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "exp_id", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "stats", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAddStr(md, PS_LIST_TAIL, "recipe", 0, NULL, "a string")) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+        if (!psMetadataAdd(md, PS_LIST_TAIL, "accept", PS_DATA_BOOL, NULL, true)) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidExpAnalysisObjectFromMetadata(md);
+        if (!object) {
+            psFree(md);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(md);
+
+        if (!object->det_id == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->iteration == -32) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->exp_id, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->stats, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (strncmp(object->recipe, "a string", MAX_STRING_LENGTH)) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+        if (!object->accept == true) {
+            psFree(object);
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/pop.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/pop.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/pop.c	(revision 7462)
@@ -0,0 +1,667 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        psF32           temp01;
+        psF32           humi01;
+        psF32           temp02;
+        psF32           humi02;
+        psF32           temp03;
+        psF32           humi03;
+        psF32           pressure;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherPop(dbh, &temp01, &humi01, &temp02, &humi02, &temp03, &humi03, &pressure)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            filter[256];
+        psF64           trans;
+        psS32           nstars;
+        psF64           ra;
+        psF64           decl;
+        psF32           exptime;
+        psF64           sky_bright;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyPop(dbh, (char **)&filter, &trans, &nstars, &ra, &decl, &exptime, &sky_bright)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            disperser_id[256];
+        psF32           atmcomp1;
+        psF32           atmcomp2;
+        psF32           atmcomp3;
+        psS32           nstars;
+        psF64           ra;
+        psF64           decl;
+        psF32           exptime;
+        psF64           sky_bright;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &nstars, &ra, &decl, &exptime, &sky_bright)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            disperser_id[256];
+        psF32           atmcomp1;
+        psF32           atmcomp2;
+        psF32           atmcomp3;
+        psF32           continuum;
+        psF32           exptime;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionPop(dbh, (char **)&disperser_id, &atmcomp1, &atmcomp2, &atmcomp3, &continuum, &exptime)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psF32           sigmax;
+        psF32           sigmay;
+        psF32           fwhm;
+        psF64           ra;
+        psF64           decl;
+        psF32           expttime;
+        char            telescope_id[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmPop(dbh, &sigmax, &sigmay, &fwhm, &ra, &decl, &expttime, (char **)&telescope_id)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psF64           sky_bright;
+        psF64           sky_var;
+        psF64           ra;
+        psF64           decl;
+        psF32           fov_x;
+        psF32           fov_y;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irPop(dbh, &sky_bright, &sky_var, &ra, &decl, &fov_x, &fov_y)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psF32           az;
+        bool            open;
+        bool            light;
+        bool            track;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domePop(dbh, &az, &open, &light, &track)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            guide[256];
+        psF32           alt;
+        psF32           az;
+        psF64           ra;
+        psF64           decl;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopePop(dbh, (char **)&guide, &alt, &az, &ra, &decl)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        char            uri[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, (char **)&uri)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        psS32           bytes;
+        char            md5sum[256];
+        char            class[256];
+        char            class_id[256];
+        char            uri[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfilePop(dbh, (char **)&exp_id, &bytes, (char **)&md5sum, (char **)&class, (char **)&class_id, (char **)&uri)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            class[256];
+        char            class_id[256];
+        char            uri[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfilePop(dbh, (char **)&exp_id, (char **)&class, (char **)&class_id, (char **)&uri)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p1_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p1_version;
+        psS32           p2_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p1_version;
+        psS32           p2_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p1_version;
+        psS32           p2_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p1_version;
+        psS32           p2_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfilePop(dbh, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe, &p1_version, &p2_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            exp_id[256];
+        char            camera[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+        char            recipe[256];
+        psS32           p2_version;
+        psS32           p3_version;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpPop(dbh, (char **)&exp_id, (char **)&camera, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats, (char **)&recipe, &p2_version, &p3_version)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        char            det_type[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunPop(dbh, (char **)&det_type)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            exp_id[256];
+        char            camera[256];
+        char            telescope[256];
+        char            exp_type[256];
+        psS32           imfiles;
+        char            filter[256];
+        char            stats[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&camera, (char **)&telescope, (char **)&exp_type, &imfiles, (char **)&filter, (char **)&stats)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        char            exp_id[256];
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfilePop(dbh, &det_id, (char **)&exp_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfilePop(dbh, &det_id, &iteration, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            comment[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFramePop(dbh, &det_id, &iteration, (char **)&comment)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        char            class_id[256];
+        char            uri[256];
+        char            stats[256];
+        char            recipe[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfilePop(dbh, &det_id, (char **)&class_id, (char **)&uri, (char **)&stats, (char **)&recipe)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            exp_id[256];
+        char            class_id[256];
+        char            stats[256];
+        char            recipe[256];
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&class_id, (char **)&stats, (char **)&recipe)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psS32           det_id;
+        psS32           iteration;
+        char            exp_id[256];
+        char            stats[256];
+        char            recipe[256];
+        bool            accept;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisPop(dbh, &det_id, &iteration, (char **)&exp_id, (char **)&stats, (char **)&recipe, &accept)) { 
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/popfits.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/popfits.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/popfits.c	(revision 7462)
@@ -0,0 +1,818 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#define TMP_FILENAME "./blargh"
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencyPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFramePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfilePopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        // allocate a temp
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisPopFits(dbh, fits, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!psFitsClose(fits)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/popobject.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/popobject.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/popobject.c	(revision 7462)
@@ -0,0 +1,566 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        weatherRow      *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = weatherPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_transparencyRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_transparencyPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_absorptionRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_absorptionPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_emissionRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_emissionPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        dimmRow         *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = dimmPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        skyp_irRow      *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = skyp_irPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        domeRow         *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = domePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        telescopeRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = telescopePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        summitExpRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = summitExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        pzPendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        pzPendingImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = pzPendingImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        newExpRow       *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = newExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        newImfileRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = newImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawDetrendExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawDetrendExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawScienceExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawScienceExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        rawImfileRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = rawImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p1PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p1PendingExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2PendingImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2PendingImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2DoneExpRow    *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p2DoneImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p2DoneImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        p3PendingExpRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = p3PendingExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detRunRow       *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detRunPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detInputExpRow  *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detInputExpPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detProcessedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detProcessedImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detStackedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detStackedImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detNormalizedImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detNormalizedImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detMasterFrameRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterFramePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detMasterImfileRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detMasterImfilePopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detResidImfileAnalysisRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidImfileAnalysisPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        detResidExpAnalysisRow *object;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        object = detResidExpAnalysisPopObject(dbh);
+        if (!object) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(object);
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/selectrowsfits.c
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/selectrowsfits.c	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/selectrowsfits.c	(revision 7462)
@@ -0,0 +1,692 @@
+#include <pslib.h>
+#include <ippdb.h>
+#include <stdlib.h>
+
+#define TMP_FILENAME "./blargh"
+
+int main ()
+{
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!weatherSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_transparencySelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_absorptionSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_emissionSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!dimmSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!skyp_irSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!domeSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!telescopeSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!summitExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!pzPendingImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!newImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawDetrendExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawScienceExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!rawImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p1PendingExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2PendingImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p2DoneImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!p3PendingExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detRunSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detInputExpSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detProcessedImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detStackedImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detNormalizedImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterFrameSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detMasterImfileSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidImfileAnalysisSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    {
+        psDB            *dbh;
+        psFits          *fits;
+
+        dbh = psDBInit("localhost", "test", NULL, "test");
+        if (!dbh) {
+            exit(EXIT_FAILURE);
+        }
+
+        fits = psFitsOpen(TMP_FILENAME, "w");
+        if (!fits) {
+            exit(EXIT_FAILURE);
+        }
+
+        if (!detResidExpAnalysisSelectRowsFits(dbh, fits, NULL, 1)) {
+            exit(EXIT_FAILURE);
+        }
+
+        psFree(fits);
+        psDBCleanup(dbh);
+    }
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/ippdb/tests/testsuite.at
===================================================================
--- /tags/rel-0_0_1/ippdb/tests/testsuite.at	(revision 7462)
+++ /tags/rel-0_0_1/ippdb/tests/testsuite.at	(revision 7462)
@@ -0,0 +1,158 @@
+m4_version_prereq([2.59])
+
+AT_INIT
+
+###
+AT_SETUP([Alloc()])
+AT_KEYWORDS([Alloc])
+
+AT_CHECK([alloc])
+
+AT_CLEANUP
+
+###
+AT_SETUP([Init()])
+AT_KEYWORDS([Init])
+
+AT_CHECK([init])
+
+AT_CLEANUP
+
+###
+AT_SETUP([Cleanup()])
+AT_KEYWORDS([Cleanup])
+
+AT_CHECK([cleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([CreateTable()])
+AT_KEYWORDS([CreateTable])
+
+# make sure the table is not there.
+AT_CHECK([dbcleanup])
+AT_CHECK([createtable])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([DropTable()])
+AT_KEYWORDS([DropTable])
+
+# make sure the table is not there.
+AT_CHECK([dbsetup])
+AT_CHECK([droptable])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([Insert()])
+AT_KEYWORDS([Insert])
+
+AT_CHECK([dbsetup])
+AT_CHECK([insert])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([Pop()])
+AT_KEYWORDS([Pop])
+
+AT_CHECK([dbsetup])
+# run insert so there is a row in the table
+AT_CHECK([insert])
+AT_CHECK([pop])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([InsertObject()])
+AT_KEYWORDS([InsertObject])
+
+AT_CHECK([dbsetup])
+AT_CHECK([insertobject])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([PopObject()])
+AT_KEYWORDS([PopObject])
+
+AT_CHECK([dbsetup])
+# run insert so there is a row in the table
+AT_CHECK([insert])
+AT_CHECK([popobject])
+AT_CHECK([dbcleanup])
+
+AT_CLEANUP
+
+###
+AT_SETUP([InsertFits()])
+AT_KEYWORDS([InsertFits])
+
+AT_CHECK([dbsetup])
+# run insert so there is a row in the table
+AT_CHECK([insert])
+# run popfis so there is afits file to read
+AT_CHECK([popfits])
+AT_CHECK([insertfits])
+AT_CHECK([dbcleanup])
+if [ test -f "blargh" ] ; then
+    rm -f "blargh"
+fi
+
+AT_CLEANUP
+
+###
+AT_SETUP([PopFits()])
+AT_KEYWORDS([PopFits])
+
+AT_CHECK([dbsetup])
+# run insert so there is a row in the table
+AT_CHECK([insert])
+AT_CHECK([popfits])
+AT_CHECK([dbcleanup])
+if [ test -f "blargh" ] ; then
+    rm -f "blargh"
+fi
+
+AT_CLEANUP
+
+###
+AT_SETUP([SelectRowsFits()])
+AT_KEYWORDS([SelectRowsFits])
+
+AT_CHECK([dbsetup])
+# run insert so there is a row in the table
+AT_CHECK([insert])
+AT_CHECK([selectrowsfits])
+AT_CHECK([dbcleanup])
+if [ test -f "blargh" ] ; then
+    rm -f "blargh"
+fi
+
+AT_CLEANUP
+
+###
+AT_SETUP([MetadataFromObject()])
+AT_KEYWORDS([MetadataFromObject])
+
+AT_CHECK([metadatafromobject])
+
+AT_CLEANUP
+
+###
+AT_SETUP([ObjectFromMetadata()])
+AT_KEYWORDS([ObjectFromMetadata])
+
+AT_CHECK([objectfrommetadata])
+
+AT_CLEANUP
+
+###
Index: /tags/rel-0_0_1/stac/.cvsignore
===================================================================
--- /tags/rel-0_0_1/stac/.cvsignore	(revision 7462)
+++ /tags/rel-0_0_1/stac/.cvsignore	(revision 7462)
@@ -0,0 +1,11 @@
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.log
+config.status
+configure
+depcomp
+install-sh
+missing
Index: /tags/rel-0_0_1/stac/Makefile.am
===================================================================
--- /tags/rel-0_0_1/stac/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/stac/Makefile.am	(revision 7462)
@@ -0,0 +1,6 @@
+SUBDIRS = src/
+
+CLEANFILES = *~ core core.*
+
+EXTRA_DIST = \
+	autogen.sh
Index: /tags/rel-0_0_1/stac/autogen.sh
===================================================================
--- /tags/rel-0_0_1/stac/autogen.sh	(revision 7462)
+++ /tags/rel-0_0_1/stac/autogen.sh	(revision 7462)
@@ -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=stac
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL=aclocal
+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: /tags/rel-0_0_1/stac/configure.ac
===================================================================
--- /tags/rel-0_0_1/stac/configure.ac	(revision 7462)
+++ /tags/rel-0_0_1/stac/configure.ac	(revision 7462)
@@ -0,0 +1,26 @@
+AC_PREREQ(2.59)
+
+AC_INIT([stac], [0.0.1], [price@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+dnl AM_CONFIG_HEADER([config.h])
+AM_MAINTAINER_MODE
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC
+AC_PROG_INSTALL
+dnl AC_PROG_LIBTOOL
+
+PKG_CHECK_MODULES([PSLIB], [pslib >= 0.9.0]) 
+
+CFLAGS="${CFLAGS}"
+stac_CFLAGS="-Wall -Werror -std=c99 -O2 -DPS_NO_TRACE"
+AC_SUBST([stac_CFLAGS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+])
+AC_OUTPUT
Index: /tags/rel-0_0_1/stac/scripts/Makefile
===================================================================
--- /tags/rel-0_0_1/stac/scripts/Makefile	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/Makefile	(revision 7462)
@@ -0,0 +1,45 @@
+# $Id: Makefile,v 1.1.1.1 2004-05-19 22:55:27 price Exp $
+#
+# make file
+#
+# Paul A. Price
+#
+
+CC = gcc
+FITSFLAGS = -lcfitsio -lm
+GSLFLAGS = -lgslcblas -lgsl
+
+### SUN:
+#FITSFLAGS = -lcfitsio -lm -L../cfitsio/ -lsocket -lnsl
+
+### RH Linux:
+CFLAGS = -Wall -O3 -g -march=i686
+
+# Destination directory
+DESTDIR = .
+
+# Inputs
+FAKEIMAGE = fakeimage.o memory.o
+CONVERTFITS = convertFITS.o memory.o
+
+.PHONY:	all clean
+
+all: fakeimage convertFITS
+
+.c.o:
+	$(CC) -c $(CFLAGS) $<
+
+fakeimage: $(FAKEIMAGE)
+	$(CC) $(FAKEIMAGE) -o $(DESTDIR)/fakeimage $(FITSFLAGS) $(GSLFLAGS)
+
+convertFITS: $(CONVERTFITS)
+	$(CC) $(CFLAGS) $(CONVERTFITS) -o $(DESTDIR)/convertFITS $(FITSFLAGS)
+
+clean:
+	-$(RM) *.o
+
+# Tags for emacs
+.PHONY : tags
+tags :
+	etags `find . \( -name \*.[ch] -o -name \*.cpp \) -print`
+
Index: /tags/rel-0_0_1/stac/scripts/README
===================================================================
--- /tags/rel-0_0_1/stac/scripts/README	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/README	(revision 7462)
@@ -0,0 +1,8 @@
+This directory is provided as a convenience (or a distraction, if you
+prefer).  It contains a few scripts used for playing with the image
+combination code, and a couple of programs for playing with FITS
+images, including one that generates a (very simple) fake image.
+
+The scripts use hard-wired paths that I have not fixed (and I have
+changed the directories in which various things reside when importing
+into CVS).  Be warned....
Index: /tags/rel-0_0_1/stac/scripts/convertFITS.c
===================================================================
--- /tags/rel-0_0_1/stac/scripts/convertFITS.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/convertFITS.c	(revision 7462)
@@ -0,0 +1,192 @@
+/*
+**
+** $Id: convertFITS.c,v 1.1.1.1 2004-05-19 22:55:27 price Exp $
+**
+** Program to convert BITPIX of FITS images.
+**
+**
+** Paul A. Price
+** 2004 January 28
+** INDNJC.
+**
+**
+** LEGAL STUFF:
+**
+** Copyright 2003, Paul A. Price.
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the GNU General Public License as
+** published by the Free Software Foundation; either version 2 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful, but
+** WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+** General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+** USA
+**
+*/
+
+
+/* Included standard header files */
+#include <stdio.h>
+#include <math.h>
+/* For getopt */
+#include <unistd.h>
+#include <getopt.h>
+/* For cfitsio */
+#include <fitsio.h>
+
+/* Included header files for components */
+#include "memory.h"
+
+/* Handy definitions */
+#define max(a,b) (((a) > (b)) ? (a) : (b))
+#define min(a,b) (((a) > (b)) ? (b) : (a))
+#define abs(a) (((a) > 0) ? (a) : (-a))
+
+
+/* Global variables */
+int verbose = 0;   /* Verbose output? */
+
+int main(int argc, char **argv)
+{
+    /* Input variables */
+    char *inputName = NULL;   /* Name of input image */
+    char *outputName = NULL;   /* Name of output image */
+    int bitpix = -32;   /* BITPIX for output image */
+
+    /* Calculation variables */
+    float *inputData;   /* The input pixels */
+    long int inputDims[2];   /* Dimensions of the input image */
+    int i;   /* Counter */
+
+    /* Variables for cfitsio */
+    int status = 0;   /* fitsio status */
+    fitsfile *infp, *outfp;   /* File pointers to FITS file */
+    int nkeys, keypos;   /* Keyword-handling variables */
+    char card[FLEN_CARD];   /* Keyword-handling variable */
+
+    /* Help function */
+    void help(void);
+    /* cfitsio error handler */
+    void ckstatus(int status);
+
+    if (argc < 3)
+    {
+        help();
+        exit(EXIT_FAILURE);
+    }
+    inputName = argv[1];
+    outputName = argv[2];
+    if (sscanf(argv[3], "%d", &bitpix) != 1)
+    {
+	help ();
+	exit (EXIT_FAILURE);
+    }
+
+
+    /* Read the input file */
+    if (verbose) printf ("Opening %s.... ", inputName);
+    if (fits_open_file(&infp,inputName,READONLY,&status)) ckstatus(status);
+    /* Get dimensions */
+    if (fits_get_img_size(infp,2,inputDims,&status)) ckstatus(status);
+    if (verbose) printf ("%ldx%ld\n",inputDims[0],inputDims[1]);
+    /* Allocate storage */
+    inputData = farray(inputDims[0]*inputDims[1]);
+    /* Read the pixels */
+    if (fits_read_img(infp, TFLOAT, 1, inputDims[0]*inputDims[1], 0, inputData, NULL, &status)) ckstatus(status);
+
+    /* Create output file */
+    if (verbose) printf ("Writing %ldx%ld output image...\n",inputDims[0],inputDims[1]);
+    if (fits_create_file(&outfp, outputName, &status)) ckstatus(status);
+
+#if 0
+    /* Write bare header */
+    if (fits_write_imghdr (outfp, bitpix, 2, inputDims, &status)) ckstatus(status);
+#endif
+
+    /* Copy FITS headers */
+    if (fits_get_hdrpos(infp, &nkeys, &keypos, &status))
+        ckstatus(status);
+    for (i=1; i <= nkeys; i++)
+    {
+        fits_read_record(infp, i, card, &status);
+        fits_write_record(outfp, card, &status);
+    }
+    ckstatus(status);
+
+    /* Write new BITPIX */
+    if (fits_update_key (outfp, TINT, "BITPIX", &bitpix, NULL, &status)) ckstatus(status);
+
+    /* Get correct BITPIX for cfitsio */
+    switch (bitpix)
+    {
+    case 8:
+    {
+	char *charData;   /* Short version of the pixels */
+	charData = carray(inputDims[0]*inputDims[1]);
+	for (i = 0; i < inputDims[0]*inputDims[1]; i++) charData[i] = (char)inputData[i];
+	/* Write pixels */
+	if (fits_write_img(outfp, TBYTE, 1, inputDims[0]*inputDims[1], charData, &status)) ckstatus(status);
+	free (charData);
+
+	break;
+    }
+    case 16:
+    {
+	short int *intData;   /* Integer version of the pixels */
+
+	intData = sarray(inputDims[0]*inputDims[1]);
+	for (i = 0; i < inputDims[0]*inputDims[1]; i++) intData[i] = (short int) inputData[i];
+	/* Write pixels */
+	if (fits_write_img(outfp, TSHORT, 1, inputDims[0]*inputDims[1], intData, &status)) ckstatus(status);
+	free (intData);
+	break;
+    }
+    default:
+	fprintf (stderr, "BITPIX must be 8 or 16\n");
+	exit (EXIT_FAILURE);
+    }
+
+    /* Close files and clean up */
+    if (fits_close_file(infp, &status)) ckstatus(status);
+    if (fits_close_file(outfp, &status)) ckstatus(status);
+    free (inputData);
+
+
+    exit (EXIT_SUCCESS);
+}
+
+
+void help(void)
+{
+    fprintf (stderr,
+	     "convertFITS [-hv] INPUT OUTPUT BITPIX\n"
+	     "\n"
+	     "\t-h      Help\n"
+	     "\t-v      Verbose\n"
+	     "\tINPUT   Input FITS file\n"
+	     "\tOUTPUT  Output FITS file\n"
+	     "\tBITPIX  BITPIX for output FITS file\n"
+	     "\n"
+	);
+}
+
+
+void ckstatus(int status)
+{
+    char msg[FLEN_ERRMSG];   /* Error message */
+
+    if (!status)
+        return;
+    fprintf (stderr,"CFITSIO returned an error code of %d\n",status);
+    while (fits_read_errmsg(msg))
+        fprintf(stderr, "CFITSIO: %s\n", msg);
+    fprintf (stderr, "Exiting....\n");
+    exit(EXIT_FAILURE);
+}
Index: /tags/rel-0_0_1/stac/scripts/fakeimage.c
===================================================================
--- /tags/rel-0_0_1/stac/scripts/fakeimage.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/fakeimage.c	(revision 7462)
@@ -0,0 +1,581 @@
+/*
+** $Id: fakeimage.c,v 1.4 2004-06-03 20:03:58 price Exp $
+** Program to make some fake images.
+**
+** Paul A. Price
+** 2003 September 3
+** INDNJC.
+**
+** Copyright (C) 2003 Paul A. Price
+**
+** This program is free software; you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation; either version 2 of the License, or
+** (at your option) any later version.
+**                                                                             
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU General Public License for more details.
+**                                                                             
+** You should have received a copy of the GNU General Public License
+** along with this program; if not, write to the Free Software
+** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+**
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <getopt.h>
+#include <fitsio.h>
+#include <math.h>
+
+/* For seeding the RNG */
+#include <sys/timeb.h>
+
+/* Memory allocation routines */
+#include "memory.h"
+
+/* GSL */
+#include <gsl/gsl_sf_erf.h>		// For error functions
+
+
+/* Conversion factor by which to multiply sigma to get FWHM.  Roughly 2.35 */
+#define SIGMAtoFWHM (2.0*sqrt((double)2.0*(log((double)2.0))))
+
+/* Errors - range of sigma to use */
+#define MAXSIGMA 4.5
+#define NUMSIGMA 100
+
+
+/* Handy functions */
+#define abs(a) (((a) > 0) ? (a) : (-a))
+#define max(a,b) (((a) > (b)) ? (a) : (b))
+#define min(a,b) (((a) > (b)) ? (b) : (a))
+
+/* Function declarations */
+void help(void);
+void ckstatus(int status);
+void addstar (float *data, int nx, int ny, int nxos, float x, float y, float flux, float seeing);
+float linint(double x[], double y[], int num, double val);   /* Linear interpolation */
+float gauss (float val);   /* Gaussian-type function */
+float fringe (int x, int y, float periodx, float periody, float phasex, float phasey);
+
+/* Global variables */
+int verbose=0;   /* Verbosity switch */
+long dims[2];   /* Dimensions of images */
+
+int main(int argc, char **argv)
+{
+    /* Input variables */
+    const char *noisefile, *outfile;   /* Input and output filenames */
+    double background;   /* Sky background, ADU */
+    double gain;   /* Detector gain, units e-/ADU */
+    double readnoise;   /* Detector read noise, units e- */
+    long overscan;   /* Number of overscan columns */
+    double zero;   /* Bias/Zero level for image */
+    double zeroslope;   /* Bias/Zero slope for image */
+    int numcrs;   /* Number of CRs */
+    int numstars;   /* Number of stars */
+    double seeing;   /* Seeing FWHM (pixels) */
+    char *starfile;   /* Name of file to use for stars */
+    int debug;   /* Output debug information?  (ie CR position and flux) */
+    int numbadpix;   /* Number of bad pixels */
+
+    /* Calculation variables */
+    fitsfile *fitsfp;   /* FITS files: input, output */
+    int status = 0;   /* fitsio status */
+    int i,x,y;   /* Counters */
+    float *outdata;   /* Output pixels */
+    float *noisedata;   /* Noise - Sigma to add to ideal image */
+    int *mask;   /* Mask of CRs */
+    float starx,stary;   /* Star centre */
+    float flux;   /* (peak) flux of star */
+    FILE *starfilefp;   /* File pointer for star file */
+    int usestarfile;   /* Boolean indicating whether we get the star data from the file or not */
+    int usenoisefile;   /* Boolean indicating whether we apply the noise or not */
+    long noisedims[2];   /* Dimensions of noise map */
+    double *gint, *gsigma;   /* Gaussian */
+    char datasec[FLEN_KEYWORD],biassec[FLEN_KEYWORD]; // Data section and bias section for FITS
+    struct timeb thetime;   /* For seeding the random number generator */
+    float fringeamp, fringeperiod, fringephase;	// Fringe amplitude, period and phase
+
+    /* Variables for getopt */
+    int opt;   /* Option, from getopt */
+    extern char *optarg;   /* Argument accompanying switch */
+    extern int optind;   /* getopt variables */
+
+    /* Seed the random number generator */
+    ftime(&thetime);
+    srand((unsigned int)thetime.millitm);
+
+    /* Set defaults for variables */
+    debug = 0;
+    gain = 1.0;
+    readnoise = 10.0;
+    background = 10000.0;
+    dims[0] = dims[1] = 1024;
+    numcrs = 0;
+    numbadpix = 0;
+    seeing = 2.5;
+    numstars = 0;
+    usestarfile=0;
+    noisefile=NULL;
+    usenoisefile = 0;
+    starfile=NULL;
+    overscan = 0;
+    zero = 0.;
+    zeroslope=0.;
+    fringeamp = 0.0;
+    fringeperiod = (float)dims[0] / 20.0;
+
+    while ((opt = getopt(argc, argv, "hvdkg:r:n:m:c:s:f:b:x:y:z:Z:o:p:F:")) != -1)
+	switch (opt)
+	{
+	  case 'h':
+	    help();
+	    exit(0);
+	  case 'v':
+	    verbose = 1;
+	    break;
+	  case 'd':
+	    debug = 1;
+	    verbose = 0;
+	    break;
+	  case 'k':
+	    usenoisefile = -1;
+	    break;
+	  case 'g':
+	    if (sscanf(optarg, "%lf", &gain) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'r':
+	    if (sscanf(optarg, "%lf", &readnoise) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'x':
+	    if (sscanf(optarg, "%ld", &dims[0]) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'y':
+	    if (sscanf(optarg, "%ld", &dims[1]) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'z':
+	    if (sscanf(optarg, "%lf", &zero) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'Z':
+	    if (sscanf(optarg, "%lf", &zeroslope) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'o':
+	    if (sscanf(optarg, "%ld", &overscan) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'c':
+	    if (sscanf(optarg, "%d", &numcrs) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 's':
+	    if (sscanf(optarg, "%d", &numstars) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    usestarfile=0;
+	    break;
+	  case 'p':
+	    if (sscanf(optarg, "%d", &numbadpix) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'n':
+	    noisefile = optarg;
+	    usenoisefile = 1;
+	    break;
+	  case 'b':
+	    if (sscanf(optarg, "%lf", &background) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'm':
+	    starfile = optarg;
+	    usestarfile=1;
+	    break;
+	  case 'f':
+	    if (sscanf(optarg, "%lf", &seeing) != 1)
+	    {
+		help();
+		exit(1);
+	    }
+	    break;
+	  case 'F':
+	    /*
+	      Note: incrementing optind, so I can read more than
+	      one parameter.
+	    */
+	    
+	    if ((sscanf(argv[optind-1], "%f", &fringeamp) != 1) ||
+		(sscanf(argv[optind++], "%f", &fringeperiod) != 1) ||
+		(sscanf(argv[optind++], "%f", &fringephase) != 1 ))
+	    {
+		help();
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  default:
+	    help();
+	    exit(1);
+	}
+
+    argc -= optind;
+    argv += optind;
+
+    if (argc != 1)
+    {
+	help();
+	exit(1);
+    }
+    
+    outfile = argv[0];
+
+
+    /* Now, the program proper... */
+
+    /* Do we read an input, or create an empty image? */
+    if (usenoisefile == 1)
+    {
+	/* Open input file */
+	if (verbose) printf ("Opening %s.... ", noisefile);
+	if (fits_open_file(&fitsfp,noisefile,READONLY,&status)) ckstatus(status);
+
+	/* Get dimensions */
+	if (fits_get_img_size(fitsfp,2,noisedims,&status)) ckstatus(status);
+	if (verbose) printf ("%ldx%ld\n",noisedims[0],noisedims[1]);
+
+	if ((noisedims[0] != dims[0] + overscan) || (noisedims[1] != dims[1]))
+	{
+	    fprintf (stderr, "Noise map must have same dimensions as output image!\n");
+	    fprintf (stderr, "Altering output image dimensions to %ldx%ld\n",noisedims[0],noisedims[1]);
+	    dims[0] = noisedims[0];
+	    dims[1] = noisedims[1];
+	}
+
+	/* Allocate storage */
+	noisedata = farray((dims[0]+overscan)*dims[1]);
+
+	/* Read the pixels */
+	if (fits_read_img(fitsfp, TFLOAT, 1, (dims[0]+overscan)*dims[1], 0, noisedata, NULL, &status)) ckstatus(status);
+
+	/* Close the file */
+	if (fits_close_file(fitsfp, &status)) ckstatus(status);
+
+    }
+    else
+    {
+	/* Integrate gaussian so we can have a nice distribution */
+	if (verbose) printf ("Creating noise map.....\n");
+	gsigma = darray(NUMSIGMA);
+	gint = darray(NUMSIGMA);
+	for (i = 0; i < NUMSIGMA; i++)
+	{
+	    gsigma[i] = 2.0*(double)(MAXSIGMA*i)/(double)NUMSIGMA - MAXSIGMA;
+	    gint[i] = gsl_sf_erf((double)gsigma[i]);
+	}
+
+	/* Allocate memory for output */
+	noisedata = farray((dims[0]+overscan)*dims[1]);   /* Output Image */
+
+	/* Create noise map */
+	for (i=0;i<(dims[0]+overscan)*dims[1];i++) noisedata[i] = (float)linint(gint,gsigma,NUMSIGMA,(double)rand()/(double)RAND_MAX*(gint[NUMSIGMA-1]-gint[0])+gint[0]);
+    }
+
+
+    /* Create empty output file */
+    if (verbose) printf ("Creating %ldx%ld output image, %s\n",dims[0]+overscan,dims[1],outfile);
+    if (fits_create_file(&fitsfp, outfile, &status)) ckstatus(status);
+
+
+    /* Initialise the output buffer to the background */
+    outdata = farray((dims[0]+overscan)*dims[1]);
+    mask = iarray((dims[0]+overscan)*dims[1]);
+    if (verbose) printf("Initialising output buffer\n");
+    for (y=0;y<dims[1];y++)
+	for (x=0;x<dims[0]+overscan;x++)
+	{
+	    /* Bias */
+	    outdata[(dims[0]+overscan)*y+x] = zero + zeroslope*(float)y;
+	    /* CR pixel mask */
+	    mask[(dims[0]+overscan)*y+x] = 0;
+	    /* Add background to the true pixels */
+	    if (x < dims[0]) outdata[(dims[0]+overscan)*y+x] += background;
+	}
+    
+    /* Add stars */
+    if (verbose) printf("Adding stars\n");
+
+    if (usestarfile)
+    {
+	if ((starfilefp = fopen(starfile,"r")) == NULL)
+	{
+	    fprintf(stderr,"Can't open %s\n",starfile);
+	    exit(1);
+	}
+
+	while (fscanf(starfilefp,"%f %f %f",&starx,&stary,&flux) == 3)
+	    addstar(&outdata[0],dims[0],dims[1],overscan,starx,stary,flux,seeing);
+	fclose(starfilefp);
+    }
+    else
+    {
+	/* Random distribution of Stars */
+	for (i=1;i<=numstars;i++)
+	{
+	    starx = (rand()/(RAND_MAX+1.0)*dims[0]);
+	    stary = (rand()/(RAND_MAX+1.0)*dims[1]);
+	    flux = rand()/(RAND_MAX+1.0)*(32768.0-background);
+
+	    addstar(&outdata[0],dims[0],dims[1],overscan,starx,stary,flux,seeing);
+	    if (verbose) printf ("%f %f %f\n",starx,stary,flux);
+	}
+    }
+
+    /* Add fringes */
+    if (fringeamp != 0.0) {
+	for (y = 0; y < dims[1]; y++)
+	    for (x = 0; x < dims[0]; x++)
+		outdata[(dims[0]+overscan)*y+x] += fringeamp * fringe(x,y,fringeperiod,fringeperiod,fringephase,fringephase);
+    }
+    
+    /* Add Poisson noise */
+    if (usenoisefile != -1)
+    {
+	if (verbose) printf ("Adding noise\n");
+	for (y=0;y<dims[1];y++)
+	    for (x=0;x<(dims[0]+overscan);x++)
+		outdata[(dims[0]+overscan)*y+x] += noisedata[(dims[0]+overscan)*y+x]*sqrt((double)(gain*(outdata[(dims[0]+overscan)*y+x]-(zero + zeroslope*y)) + readnoise*readnoise))/gain;
+    }
+
+    /* Random distribution of bad pixels */
+    if (verbose) printf ("Adding bad pixels\n");
+    for (i = 1; i <= numbadpix; i++)
+    {
+	x = (int)(rand()/(RAND_MAX+1.0)*dims[0]);
+	y = (int)(rand()/(RAND_MAX+1.0)*dims[1]);
+	outdata[(dims[0]+overscan)*y+x] = 0.0;
+    }
+
+    /* Random distribution of CRs */
+    if (verbose) printf ("Adding CRs\n");
+    if (numcrs > dims[0]*dims[1]/2) fprintf (stderr,"Image too small (%ldx%ld) for %d CRs --- no CRs added.\n",dims[0],dims[1],numcrs);
+    else
+    {
+	for (i=1;i<=numcrs;i++)
+	{
+	    x = (int)(rand()/(RAND_MAX+1.0)*dims[0]);
+	    y = (int)(rand()/(RAND_MAX+1.0)*dims[1]);
+	    if (mask[(dims[0]+overscan)*y+x]) i--;   /* Get another one */
+	    else
+	    {
+		flux = rand()/(RAND_MAX+1.0)*(32768.0-background);
+		outdata[(dims[0]+overscan)*y+x] += flux;
+		mask[(dims[0]+overscan)*y+x] = 1;
+		if (debug) printf ("%d %d %f\n",x,y,flux);
+	    }
+	}
+    }
+
+    /* Write output */
+    if (verbose) printf ("Writing %ldx%ld output image...\n",dims[0]+overscan,dims[1]);
+    dims[0] += overscan;
+    if (fits_write_imghdr (fitsfp, -32, 2, dims, &status)) ckstatus(status);
+
+    /* Write DATASEC, BIASSEC */
+    (void)sprintf (datasec,"[%ld:%ld,%ld:%ld]",1L,dims[0]-overscan,1L,dims[1]);
+    (void)sprintf (biassec,"[%ld:%ld,%ld:%ld]",dims[0]-overscan+1,dims[0],1L,dims[1]);
+    if (fits_write_key(fitsfp, TSTRING, "DATASEC", datasec,"Data section",&status)) ckstatus(status);
+    if (fits_write_key(fitsfp, TSTRING, "BIASSEC", biassec,"Bias section",&status)) ckstatus(status);
+
+    /* Write the FITS file */
+    if (fits_write_img(fitsfp, TFLOAT, 1, dims[0]*dims[1], outdata, &status)) ckstatus(status);
+
+    /* Close files and clean up */
+    if(fits_close_file(fitsfp, &status)) ckstatus(status);
+    free ((float*)outdata);
+    free ((int*)mask);
+
+    if (verbose) printf ("Done!\n");
+
+    exit(0);
+}
+
+
+/* Functions follow */
+
+void help(void)
+{
+    fprintf(stderr,
+	    "fakeimage [-hvd] [-x NX] [-y NY] [-g GAIN] [-r READNOISE] [-b BACKGROUND] [-s NUM_STARS] [-f FWHM] [-c NUM_CRS] [-p NUM_BADPIX] [-z ZERO_LEVEL] [-o OVERSCAN] [-n NOISE_MAP] OUTPUT_IMAGE\n"
+	    "\n"
+	    "\tOUTPUT_IMAGE:   Output file (FITS file)\n"
+	    "\t-i INPUT_IMAGE: Use input image (FITS file)\n"
+	    "\t-x NX; -y NY:   Size of the created image\n"
+	    "\t-g GAIN:        Detector gain (e-/ADU)\n"
+	    "\t-r READNOISE:   Detector read noise (e-)\n"
+	    "\t-b BACKGROUND:  Sky background (ADU)\n"
+	    "\t-s NUM_STARS:   Number of stars on the image\n"
+	    "\t-m STARFILE:    Use file (x,y,flux) for stars\n"
+	    "\t-f FWHM:        Seeing FWHM\n"
+            "\t-c NUM_CRS:     Number of CRs on the image\n"
+	    "\t-p NUM_BADPIX:  Number of bad pixels on image\n"
+	    "\t-n NOISE_MAP:   Name of an image to use for the noise\n"
+	    "\t-z ZERO_LEVEL:  Bias/Zero level to add to image\n"
+	    "\t-Z ZERO_SLOPE:  Bias/Zero slope as a function of row\n"
+	    "\t-o OVERSCAN:    Number of overscan columns to add\n"
+	    "\t-k:             Klean --- no noise added\n"
+	    "\t-h:             Help\n"
+	    "\t-v:             Verbose\n"
+	    "\t-d:             Debug (ie output CR flux, position)\n"
+	    "\n"
+	);
+}
+
+void ckstatus(int status)
+{
+    char msg[81];
+    
+    if (!status)
+	return;
+    while (fits_read_errmsg(msg))
+	fprintf(stderr, "CFITSIO: %s\n", msg);
+    exit(1);
+}
+
+
+/* Finds a value in an array --- note: zero-indexed */
+int find(double value,			// Value to find
+	 double *array,			// Array to find in
+	 int n				// Number of values in array
+    )
+{
+    int low = 0;			// Low-end bracket index
+    int high = n - 1;			// High-end bracket index
+    int index;				// Index to check
+
+    /* Test the ends */
+    if (array[high] < value) {
+	return high - 1;
+    }
+    if (array[low] > value) {
+	return low;
+    }
+
+    /* Bisect */
+    do {
+	index = (high - low) / 2 + low;
+	if (array[index] > value) {
+	    high = index;
+	} else {
+	    low = index;
+	}
+    } while (high > low + 1);
+
+    return low;
+}
+
+
+
+
+float linint(double x[], double y[], int num, double val)
+{
+    /*
+      Returns the value of the function y = f(x) at val,
+      calculated by linear interpolation.
+      There are num values in arrays x and y.
+    */
+
+    int index = find(val, x, num);	// Index of position in x
+    double ans = y[index] + (y[index+1] - y[index]) * (val - x[index]) / (x[index+1] - x[index]);
+
+    return (ans);
+}
+
+float gauss (float val)
+{
+    /*
+      Returns a normal-shaped curve evaulated at val.
+    */
+    return (exp((double)(-0.5*val*val)));
+}
+
+
+void addstar (float *data, int nx, int ny, int nxos, float x, float y, float flux, float seeing)
+/*
+  Adds a Gaussian to the data
+
+  data: The pixels
+  nx, ny: Dimensions
+  nxos: Size of overscan (don't put star here)
+  x, y: Centre of the star
+  flux: peak flux of the star
+  seeing: FWHM of the star
+*/
+{
+    int j,k;   /* Counters */
+    int minj,maxj,mink,maxk;   /* Ranges */
+
+    /* Get range for star */
+    minj = max(0,(int) (x-seeing/SIGMAtoFWHM*MAXSIGMA));
+    maxj = min(nx-1,(int) (x+seeing/SIGMAtoFWHM*MAXSIGMA));
+    mink = max(0,(int) (y-seeing/SIGMAtoFWHM*MAXSIGMA));
+    maxk = min(ny-1,(int) (y+seeing/SIGMAtoFWHM*MAXSIGMA));
+
+    /* Put the star in. */
+    for (j=minj;j<=maxj;j++)
+	for (k=mink;k<=maxk;k++)
+	    data[(nx+nxos)*k+j] += flux/sqrt(2.0*M_PI)/(seeing/SIGMAtoFWHM)*exp(-1.0 * ((x-j)*(x-j) + (y-k)*(y-k))/2.0/(seeing/SIGMAtoFWHM)/(seeing/SIGMAtoFWHM));
+    
+}
+
+/* Return a "fringe" value for a pixel */
+float fringe (int x, int y,		// Pixel coordinates
+	      float periodx, float periody, // Period in x and y
+	      float phasex, float phasey// Phase for x and y
+    )
+{
+    float xval = 2.0*M_PI/periodx * (float)x + phasex;
+    float yval = 2.0*M_PI/periody * (float)y + phasey;
+
+    return cos(xval)*cos(yval);
+}
+
+
Index: /tags/rel-0_0_1/stac/scripts/memory.c
===================================================================
--- /tags/rel-0_0_1/stac/scripts/memory.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/memory.c	(revision 7462)
@@ -0,0 +1,238 @@
+/*
+** memory.c
+**
+** Handy little functions to allocate memory for arrays etc.
+**
+**
+** CVS: $Id: memory.c,v 1.1.1.1 2004-05-19 22:55:27 price Exp $
+**
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <string.h>
+#include "memory.h"
+
+/*
+  Function to return a pointer to a generic object, such as a struct.
+*/
+
+void *papalloc (size_t size)
+{
+    void *newobject = malloc(size);
+    if (! newobject)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n",strerror(errno));
+	abort();
+    }
+    
+    return (newobject);
+}
+	
+
+
+/*
+  Subroutine to allocate memory for an array of doubles of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+double *darray (size_t size)
+{
+    double *thearray = malloc(size*sizeof(double));
+    if (!thearray)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n",strerror(errno));
+	abort();
+    }
+    
+    return (thearray);
+}
+			     
+
+/*
+  Subroutine to allocate memory for an array of floats of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+float *farray (size_t size)
+{
+    float *thearray = malloc(size*sizeof(float));
+    if (!thearray)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n",strerror(errno));
+	abort();
+    }
+
+    return (thearray);
+}
+
+
+/*
+  Subroutine to allocate memory for an array of integers of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+int *iarray (size_t size)
+{
+    int *thearray = malloc(size*sizeof(int));
+    if (!thearray)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n", strerror(errno));
+	abort();
+    }
+    
+    return (thearray);
+}
+
+/*
+  Subroutine to allocate memory for an array of chars of the indicated
+  size.  Array is zero-offset (pass size+1 for unit-offset array).
+*/
+char *carray (size_t size)
+{
+    char *thearray = malloc(size);   /* Since sizeof(char) == 1 by definition */
+    if (!thearray)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n", strerror(errno));
+	abort();
+    }
+    
+    return (thearray);
+}
+
+/*
+  Subroutine to allocate memory for an array of short ints of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+short int *sarray (size_t size)
+{
+    short int *thearray = malloc(size*sizeof(short int));
+    if (!thearray)
+    {
+	fprintf (stderr, "Error allocating memory for array: %s\n", strerror(errno));
+	abort();
+    }
+    
+    return (thearray);
+}
+
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix of
+  floats.  Matrix is zero-offset (pass size+1 for unit-offset matrix).
+*/
+float **fmatrix (size_t m, size_t n)
+{
+    int i;
+    float **thematrix = (float**)malloc(m*sizeof(float*));
+    if (!thematrix)
+    {
+	fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	abort();
+    }
+
+    for(i=0;i<m;i++)
+    {
+	thematrix[i] = malloc(n*sizeof(float));
+	if (!thematrix[i])
+	{
+	    fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	    abort();
+	}
+    }
+
+    return thematrix;
+}
+
+
+
+
+
+/*
+  Subroutine to allocate memory for a square matrix of doubles of the
+  indicated size.  Matrix is zero-offset (pass size+1 for unit-offset
+  matrix).
+*/
+double **squarematrix (size_t size)
+{
+    int i;
+    double **thematrix = (double**)malloc(size*sizeof(double*));
+    if (!thematrix)
+    {
+	fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	abort();
+    }
+    
+    for(i=0;i<size;i++)
+    {
+	thematrix[i] = malloc(size*sizeof(double));
+	if (!thematrix[i])
+	{
+	    fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	    abort();
+	}
+    }
+  
+    return thematrix;
+}
+
+
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix of
+  doubles.  Matrix is zero-offset (pass size+1 for unit-offset matrix).
+*/
+double **rectmatrix (size_t m, size_t n)
+{
+    int i;
+    double **thematrix = (double**)malloc(m*sizeof(double*));
+    if (!thematrix)
+    {
+	fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	abort();
+    }
+
+    for(i=0;i<m;i++)
+    {
+	thematrix[i] = malloc(n*sizeof(double));
+	if (!thematrix[i])
+	{
+	    fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	    abort();
+	}
+    }
+
+    return thematrix;
+}
+
+
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix.
+  Matrix is zero-offset (pass size+1 for unit-offset matrix).  This
+  function adds a buffer of 1 pixel around the edge (i.e. it runs from
+  -1 to m instead of 0 to m-1) --- provided by Nick Kaiser.
+*/
+double **padrectmatrix(size_t m, size_t n)
+{
+
+    int i;
+    double **thematrix = (double **)malloc((m+2)*sizeof(double*)) + 1;
+    if (!thematrix)
+    {
+	fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	abort();
+    }
+
+    for(i=-1;i<=m;i++)
+    {
+	thematrix[i] = malloc((n+2)*sizeof(double)) + 1;
+	if (!thematrix[i])
+	{
+	    fprintf (stderr, "Error allocating memory for matrix: %s\n", strerror(errno));
+	    abort();
+	}
+    }
+
+    return thematrix;
+}
+
Index: /tags/rel-0_0_1/stac/scripts/memory.h
===================================================================
--- /tags/rel-0_0_1/stac/scripts/memory.h	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/memory.h	(revision 7462)
@@ -0,0 +1,83 @@
+/*
+** memory.h
+**
+** Header file for memory.c
+**
+** CVS: $Id: memory.h,v 1.1.1.1 2004-05-19 22:55:27 price Exp $
+**
+*/
+
+#ifndef _MEMORY_H
+#define _MEMORY_H
+
+
+void *papalloc (size_t size);
+/*
+  Function to return a pointer to a generic object, such as a struct.
+*/
+
+double *darray (size_t size);
+/*
+  Subroutine to allocate memory for an array of doubles of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+
+float *farray (size_t size);
+/*
+  Subroutine to allocate memory for an array of floats of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+
+int *iarray (size_t size);
+/*
+  Subroutine to allocate memory for an array of integers of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+
+short int *sarray (size_t size);
+/*
+  Subroutine to allocate memory for an array of short ints of the
+  indicated size.  Array is zero-offset (pass size+1 for unit-offset
+  array).
+*/
+
+char *carray (size_t size);
+/*
+  Subroutine to allocate memory for an array of chars of the indicated
+  size.  Array is zero-offset (pass size+1 for unit-offset array).
+*/
+
+float **fmatrix (size_t m, size_t n);
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix of
+  floats.  Matrix is zero-offset (pass size+1 for unit-offset matrix).
+*/
+
+double **squarematrix (size_t size);
+/*
+  Subroutine to allocate memory for a square matrix of doubles of the
+  indicated size.  Matrix is zero-offset (pass size+1 for unit-offset
+  matrix).
+*/
+
+double **rectmatrix (size_t m, size_t n);
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix of
+  doubles.  Matrix is zero-offset (pass size+1 for unit-offset matrix).
+*/
+
+double **padrectmatrix(size_t m, size_t n);
+/*
+  Subroutine to allocate memory for an m x n rectangular matrix.
+  Matrix is zero-offset (pass size+1 for unit-offset matrix).  This
+  function adds a buffer of 1 pixel around the edge (i.e. it runs from
+  -1 to m instead of 0 to m-1) --- provided by Nick Kaiser.
+*/
+
+
+
+
+#endif
Index: /tags/rel-0_0_1/stac/scripts/mkfakeimages.pl
===================================================================
--- /tags/rel-0_0_1/stac/scripts/mkfakeimages.pl	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/mkfakeimages.pl	(revision 7462)
@@ -0,0 +1,159 @@
+#!/usr/local/bin/perl
+#
+# testcombine.pl
+#
+# Program to create some dithered/rotated frames and combine them
+# to see how the combination works.
+#
+#
+# Copyright (C) 2003 Paul A. Price
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#                                                                             
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#                                                                             
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+#
+#
+# CVS: $Id: mkfakeimages.pl,v 1.2 2004-05-20 00:15:57 price Exp $
+#
+# History:
+# Added orderly star placement.  Fixed so that the star positions are
+# accurate once mapped.  --- PAP, 20/10/03.
+# Allow different seeing between different images --- PAP 19/5/04.
+#
+
+
+# Parameters.
+$DEG_IN_RAD = 57.2957795130823;   # Degrees per radian.
+
+$GAIN = 1;			# Number of electrons per ADU
+$READNOISE = 10;		# Detector read noise (electrons);
+$NUMSTARS = 500;		# Number of stars on images.
+$INSIZE = 1024;		    # Size of input images (pixels on a side).
+$OUTSIZE = 1024;	    # Size of output image (pixels on a side).
+$SEEING = 0; # Seeing in input images (input pixels).  Zero means use array.
+$NUMCRS = 0;			# Number of CRs on input images.
+$STARFILE = "stars";	      # File containing list of stars (x,y,f).
+$IMAGE = "test";		# Base file name for images and stuff.
+$BACKGROUND = 10000;		# Sky background for input images
+$OUTSCALE = sqrt(1);  # Pixel scale of output image compared to input.
+$STARANGLE = 1.5; # Angle at which a regular star field is tilted (degrees)
+$STARFLUXMIN = 500;		# Minimum flux of stars (ADU)
+$STARFLUXMAX = 20000;		# Maximum flux of stars (ADU)
+$MAXOFFSET = 5;	 # Maximum offset for each image (output image scale).
+@ROTATIONS=(0,3);		# Rotations of the images
+@SEEING = (2.5, 4.5);	       # Seeing in input images (input pixels)
+
+
+$CREATE = "./fakeimage -d -x $INSIZE -y $INSIZE -g $GAIN -r $READNOISE -b $BACKGROUND";   # Command to create an input image
+$CREATELIST = "-m ";   # Switch to take stars from a list.
+$CREATESEEING = "-f ";   # Switch for seeing
+
+# Parse command-line arguments
+for ($i=0;$i<=$#ARGV;$i++)
+{
+    if ($ARGV[$i] eq "-numstars") { $NUMSTARS = $ARGV[++$i]; }
+    elsif ($ARGV[$i] eq "-insize") { $INSIZE = $ARGV[++$i]; }
+    elsif ($ARGV[$i] eq "-outsize") { $OUTSIZE = $ARGV[++$i]; }
+    elsif ($ARGV[$i] eq "-seeing") { $SEEING = $ARGV[++$i]; }
+    elsif ($ARGV[$i] eq "-numcrs") { $NUMCRS = $ARGV[++$i]; }
+    elsif ($ARGV[$i] eq "-switches") { $COMBINESWITCHES = $ARGV[++$i]; }
+    else
+    {
+	die "Unknown command-line switch: $ARGV[$i]\n";
+    }
+}
+
+
+
+# Generate list of stars.
+@x=();   # List of star x positions
+@y=();   # List of star y positions
+@f=();   # List of star fluxes
+open STARS, "> $IMAGE.stars";
+$num=0;
+for ($i=0;$i<sqrt($NUMSTARS);$i++)
+{
+    # Random distribution
+#    push @x, rand($OUTSIZE);
+#    push @y, rand($OUTSIZE);
+#    push @f, rand(32*1024-$BACKGROUND);
+
+    # Ordered distribution
+    for ($j=0;$j<sqrt($NUMSTARS);$j++)
+    {
+	push @x, ($i+1)*$OUTSIZE/(sqrt($NUMSTARS)+1)*cos($STARANGLE/$DEG_IN_RAD) + ($j+1)*$OUTSIZE/(sqrt($NUMSTARS)+1)*sin($STARANGLE/$DEG_IN_RAD);
+	push @y, - ($i+1)*$OUTSIZE/(sqrt($NUMSTARS)+1)*sin($STARANGLE/$DEG_IN_RAD) + ($j+1)*$OUTSIZE/(sqrt($NUMSTARS)+1)*cos($STARANGLE/$DEG_IN_RAD);
+	if ($STARFLUXMIN == $STARFLUXMAX) { push @f, $STARFLUXMAX; }
+	else { push @f, rand($STARFLUXMAX-$STARFLUXMIN) + $STARFLUXMIN; }
+
+	print STARS "$x[$num] $y[$num] $f[$num]\n";
+	$num++;
+    }
+}
+
+
+# Iterate over the input images
+for ($i=0;$i<=$#ROTATIONS;$i++)
+{
+    # Get transformation.
+    $B00 = cos($ROTATIONS[$i]/$DEG_IN_RAD) / $OUTSCALE;
+    $B01 = sin($ROTATIONS[$i]/$DEG_IN_RAD) / $OUTSCALE;
+    $B10 = - $B01;
+    $B11 = $B00;
+    $A0 = $INSIZE/2;
+    $A1 = $INSIZE/2;
+    if ($MAXOFFSET == 0)
+    {
+	$offsetx = 0;
+	$offsety = 0;
+    }
+    else
+    {
+	$offsetx = rand(2*$MAXOFFSET) - $MAXOFFSET;
+	$offsety = rand(2*$MAXOFFSET) - $MAXOFFSET;
+    }
+
+    # Output star positions to file
+    open STARS, "> ${IMAGE}_$i.stars";
+    for ($j=0;$j<$num;$j++)
+    {
+	$x = $B00*($x[$j] - 1.0 - $OUTSIZE/2 - $offsetx) + $B10*($y[$j] - 1.0 - $OUTSIZE/2 - $offsety) + $A0;
+	$y = $B01*($x[$j] - 1.0 - $OUTSIZE/2 - $offsetx) + $B11*($y[$j] - 1.0 - $OUTSIZE/2 - $offsety) + $A1;
+
+
+	print STARS "$x $y $f[$j]\n";
+    }
+    close STARS;
+
+
+    # Create image
+    if ($SEEING == 0) { $seeing = $SEEING[$i]; }
+    else { $seeing = $SEEING; }
+    print "$CREATE $CREATESWITCHES $CREATELIST ${IMAGE}_$i.stars $CREATESEEING $seeing ${IMAGE}_$i.fits\n";
+    system "$CREATE $CREATESWITCHES $CREATELIST ${IMAGE}_$i.stars $CREATESEEING $seeing ${IMAGE}_$i.fits";
+
+    # Create map
+    $B00 = $OUTSCALE*cos($ROTATIONS[$i]/$DEG_IN_RAD);
+    $B01 = - $OUTSCALE*sin($ROTATIONS[$i]/$DEG_IN_RAD);
+    $B10 = - $B01;
+    $B11 = $B00;
+
+    $A0=$OUTSIZE/2 - ($B00 + $B10)*$INSIZE/2 + $offsetx;
+    $A1=$OUTSIZE/2 - ($B01 + $B11)*$INSIZE/2 + $offsety;
+
+    # Output map
+    open MAP, "> ${IMAGE}_$i.fits.map";
+    print MAP "$A0 $A1\n$B00 $B01\n$B10 $B11\n";
+    close MAP;
+}
+
Index: /tags/rel-0_0_1/stac/scripts/testMap.pl
===================================================================
--- /tags/rel-0_0_1/stac/scripts/testMap.pl	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/testMap.pl	(revision 7462)
@@ -0,0 +1,54 @@
+#!/usr/local/bin/perl
+
+open STARS, "test.stars";
+my @xSky = ();
+my @ySky = ();
+while (<STARS>) {
+    my ($x, $y, $flux) = split;
+    push @xSky, $x;
+    push @ySky, $y;
+}
+
+for (my $i = 0; $i < 4; $i++) {
+    open MAP, "test_$i.fits.map";
+    my $line = <MAP>;
+    ($A0, $A1) = split /\s+/, $line;
+    my $line = <MAP>;
+    ($B00, $B01) = split /\s+/, $line;
+    my $line = <MAP>;
+    ($B10, $B11) = split /\s+/, $line;
+    close MAP;
+
+    open STARS, "test_$i.stars";
+    my @diff = ();
+    my $num = 0;
+    while (<STARS>) {
+	my ($x, $y, $flux) = split;
+
+	$xPrime = $A0 + $B00*$x + $B10*$y;
+	$yPrime = $A1 + $B01*$x + $B11*$y;
+
+	push @xdiff, $xPrime - $xSky[$num];
+	push @ydiff, $yPrime - $ySky[$num];
+
+	$num++;
+    }
+
+    print "File: $i, Difference: " . mean(\@xdiff) . ", " . mean(\@ydiff) . "\n";
+}
+
+### ENDS
+
+
+# Return the mean
+sub mean
+{
+    my ($arrayRef) = @_;
+
+    my @array = @$arrayRef;
+    my $mean = 0;
+    foreach my $value (@array) {
+	$mean += $value;
+    }
+    return $mean / (scalar @array);
+}
Index: /tags/rel-0_0_1/stac/scripts/testOffsets.pl
===================================================================
--- /tags/rel-0_0_1/stac/scripts/testOffsets.pl	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/testOffsets.pl	(revision 7462)
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+#
+# testOffsets.pl
+# Concerned that different offsets are producing different sensitivity to finding CRs.
+# For example, half pixel offsets in each direction seem to produce lots of false positives,
+# but tiny offsets produce few CRs found.  Need statistics.
+
+my @offsets = ();
+my @numcr = ();
+my $badNum = 0;
+for (my $iter = 0; $iter < 500; $iter++) {
+    system "./testParams.pl";
+    my $bad = 0;
+    for (my $i = 0; $i < 4; $i++) {
+
+	# Number of CRs found
+	my $wc = `wc -l test_$i.fits.cr`;
+	my ($number) = ($wc =~ /^\s*(\d+)/);
+	print "Found $number CRs in image $i\n";
+	if ($number > 2000) {
+	    $bad = 1;
+	}
+
+	# Offset
+	my $mapFile;
+	open $mapFile, "test_$i.fits.map";
+	my @map = <$mapFile>;
+	print @map . "\n";
+	close $mapFile;
+	my ($x) = ($map[1] =~ /^(\S+)/);
+	my ($y) = ($map[2] =~ /^(\S+)/);
+	my $offset = sqrt($x**2 + $y**2);
+	print "Offset for image $i is $offset\n";
+
+	push @offsets, $offset;
+	push @numcr, $number;
+    }
+
+    if ($bad) {
+	$badNum++;
+	system "mkdir bad_$badNum";
+	system "cp -f test_* testout.fits* bad_$badNum";
+    }
+}
+
+my $cr;
+open $cr, "> cr.dat";
+for (my $i = 0; $i <= $#offsets; $i++) {
+    print $cr "$offsets[$i] $numcr[$i]\n";
+}
+close $cr;
+system "sort -k 1 -n cr.dat > cr.dat.sort";
+
+__END__
Index: /tags/rel-0_0_1/stac/scripts/testParams.pl
===================================================================
--- /tags/rel-0_0_1/stac/scripts/testParams.pl	(revision 7462)
+++ /tags/rel-0_0_1/stac/scripts/testParams.pl	(revision 7462)
@@ -0,0 +1,176 @@
+#!/usr/bin/perl
+#
+# testParams.pl
+# Program to run through the parameters for STAC.
+
+
+@REJECT = (3.0); # List of "reject" parameters
+@FRAC = (0.5); # List of "frac" parameters
+@GRAD = (0.6); # List of "grad" parameters
+@SEEING = (2.5); # List of seeing
+$ITERATIONS = 1; # Number of iterations for each parameter combination
+
+$RUN = "rm -f testout.fits* chi2_*.fits test_[0-3].fits.\{err,grad,mask,rejmap,shift*}; ./stac -v testout.fits test_0.fits test_1.fits test_2.fits test_3.fits";		# Command with which to run the STAC program
+$RUNREJECT = "-k";		# Rejection parameter flag
+$RUNFRAC = "-f";		# Frac parameter flag
+$RUNGRAD = "-G";		# Grad parameter flag
+$CREATEDIR = "/home/mithrandir/price/images/"; # Directory in which the image creation routines reside
+$CREATE = "./mkfakeimages.pl -diag"; # Command for the image creation
+$CREATESEEING = "-seeing";	# Switch for seeing in the image creation
+$CREATENOCRS = "-numcrs 0";	# Switch for no CRs in the image creation
+
+$WORKINGDIR = `pwd`;		# Working directory (current directory)
+chomp($WORKINGDIR);
+
+# Iterate over the parameter space
+open OUTPUT, "> testParams.out";
+foreach $seeing (@SEEING) {
+    foreach $reject (@REJECT) {
+	foreach $frac (@FRAC) {
+	    foreach $grad (@GRAD) {
+		@trueRejected = (); # Number of true CRs rejected in each iteration
+		@falseRejected = (); # Number of false CRs rejected in each iteration
+		@dBonB = (); # Relative difference between input and output slope
+		
+		# Do each iteration
+		for ($i=1;$i<=$ITERATIONS;$i++) {
+		    
+		    ### Run the program on the data with CRs
+		    
+		    # Make the data
+		    chdir $CREATEDIR;
+		    print "$CREATE $CREATESEEING $seeing\n";
+		    system "$CREATE $CREATESEEING $seeing";
+		    system "mv -f test_[0-3]\{.fits,.fits.map,.cr\} $WORKINGDIR";
+		    chdir $WORKINGDIR;
+		    
+		    # Run the program and count the number of CRs
+		    print "$RUN $RUNREJECT $reject $RUNFRAC $frac $RUNGRAD $grad |\n";
+		    open STAC, "$RUN $RUNREJECT $reject $RUNFRAC $frac $RUNGRAD $grad |";
+		    $numCRs = 0;	# Number of CRs masked
+		    while (<STAC>) {
+			print;
+			if (/(\d+) pixels masked in image \d+/) {
+			    $numCRs += $1;
+			}
+		    }
+		    close STAC;
+		    printf "---> %d pixels masked in total.\n", $numCRs;
+
+		    my @trueFlux = (); # Flux of pixels that are real CRs
+		    my @falseFlux = ();	# Flux of pixels that were masked, but not CRs
+		    for (my $i = 0; $i < 4; $i++) {
+			# Read list of CRs injected into image
+			open CR, "test_$i.cr";
+			my %originalCRs = ();
+			while (<CR>) {
+			    my ($x,$y,$flux) = split;
+			    $originalCRs{"$x,$y"} = $flux;
+			}
+			close CR;
+
+			# Read list of CRs masked by program
+			open CR, "test_$i.fits.cr";
+			while (<CR>) {
+			    my ($x,$y,$junk,$flux,$frac,$grad) = split;
+			    # See if it was injected
+			    if (exists($originalCRs{"$x,$y"})) {
+				push @trueFlux, $flux;
+			    } else {
+				push @falseFlux, $flux;
+			    }
+			}
+		    }
+
+		    # Number of true and false CRs
+		    push @trueRejected, scalar @trueFlux;
+		    push @falseRejected, scalar @falseFlux;
+
+		    print "True: " . scalar @trueFlux . "   False: " . scalar @falseFlux . "\n";
+
+		    # Get input slope
+		    system "cat test_[0-3].cr | sort -k 3 -n | awk \'{num++; print num,\$3;}\' > test_in.cr";
+		    open FIT, "guessline.pl test_in.cr |";
+		    while (<FIT>) {
+			if (/b = (\S+)/) {
+			    $inSlope = $1;
+			    last;
+			}
+		    }
+		    close FIT;
+
+		    # Get output slope
+		    open CR, "| sort -n | awk \'{num++; print num,\$1;}\' > test_out.cr";
+		    foreach $flux (@trueFlux) {
+			print CR $flux . "\n";
+		    }
+		    close CR;
+		    
+		    open FIT, "guessline.pl test_out.cr |";
+		    while (<FIT>) {
+			if (/b = (\S+)/) {
+			    $outSlope = $1;
+			    last;
+			}
+		    }
+		    close FIT;
+		    print "inSlope: $inSlope\noutSlope: $outSlope\n";
+
+		    push @dBonB, abs($outSlope - $inSlope) / $inSlope;
+		    
+		}
+		
+		# Calculate the mean
+		$trueMean = mean(\@trueRejected);
+		$falseMean = mean(\@falseRejected);
+		$trueMedian = median(\@trueRejected);
+		$falseMedian = median(\@falseRejected);
+		$dBonBMean = mean(\@dBonB);
+		$dBonBMedian = median(\@dBonB);
+		
+		printf "$seeing\t$reject\t$frac\t$grad\t%.1f\t%.1f\t%.1f\t%.1f\t%.3f\t%.3f\n",$trueMean,$trueMedian,$falseMean,$falseMedian,$dBonBMean,$dBonBMedian;
+		printf OUTPUT "$seeing\t$reject\t$frac\t$grad\t%.1f\t%.1f\t%.1f\t%.1f\t%.3f\t%.3f\n",$trueMean,$trueMedian,$falseMean,$falseMedian,$dBonBMean,$dBonBMedian;
+	    }
+	}
+    }
+}
+close OUTPUT;
+
+
+
+
+# Return the mean
+sub mean
+{
+    my ($arrayRef) = @_;
+
+    my @array = @$arrayRef;
+    my $mean = 0;
+    foreach my $value (@array) {
+	$mean += $value;
+    }
+    return $mean / (scalar @array);
+}
+
+# Return the median
+sub median
+{
+    my ($arrayRef) = @_;
+    my @array = @$arrayRef;
+    # Insertion sort
+    my @sort = ();
+    foreach my $value (@array) {
+	for ($i=0;$i<=$#sort;$i++) {
+	    if ($sort[$i] < $value) {
+		last;
+	    }
+	}
+	splice @sort, $i, 0, $value;
+    }
+    # Median is the middle one.
+    if (scalar @sort % 2 == 1) {
+	return $sort[(scalar @sort  - 1) / 2];
+    } else {
+	return ($sort[(scalar @sort)/2] + $sort[(scalar @sort)/2 - 1])/2;
+    }
+}
Index: /tags/rel-0_0_1/stac/src/.cvsignore
===================================================================
--- /tags/rel-0_0_1/stac/src/.cvsignore	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/.cvsignore	(revision 7462)
@@ -0,0 +1,9 @@
+.deps
+Makefile
+Makefile.in
+calcGradient
+combine
+shift
+shiftSize
+stac
+sum
Index: /tags/rel-0_0_1/stac/src/Makefile.am
===================================================================
--- /tags/rel-0_0_1/stac/src/Makefile.am	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/Makefile.am	(revision 7462)
@@ -0,0 +1,76 @@
+# Compilation flags for Automake
+AM_CPPFLAGS = $(PSLIB_CFLAGS) $(stac_CFLAGS)
+AM_LDFLAGS = $(PSLIB_LIBS)
+
+# Programs to install in the "bin" directory
+bin_PROGRAMS = \
+	calcGradient \
+	combine \
+	shift \
+	shiftSize \
+	stac \
+	sum
+
+# Header files that don't need to be installed
+noinst_HEADERS = \
+	combineConfig.h \
+	stac.h \
+	stacConfig.h
+
+# List of sources for each of the programs
+stac_SOURCES = \
+	stac.c \
+	stacAreaOfInterest.c \
+	stacConfig.c \
+	stacCombine.c \
+	stacCheckMemory.c \
+	stacErrorImages.c \
+	stacHelp.c \
+	stacInvertMaps.c \
+	stacRead.c \
+	stacRejection.c \
+	stacScales.c \
+	stacSize.c \
+	stacTime.c \
+	stacTransform.c
+
+calcGradient_SOURCES = \
+	calcGradient.c \
+	stacRejection.c
+
+combine_SOURCES = \
+	combine.c \
+	combineConfig.c \
+	stacCheckMemory.c \
+	stacCombine.c \
+	stacErrorImages.c \
+	stacRead.c \
+	stacScales.c \
+	stacTime.c
+
+shift_SOURCES = \
+	shift.c \
+	stacCheckMemory.c \
+	stacInvertMaps.c \
+	stacRead.c \
+	stacSize.c \
+	stacTransform.c
+
+shiftSize_SOURCES = \
+	shiftSize.c \
+	stacCheckMemory.c \
+	stacConfig.c \
+	stacRead.c \
+	stacSize.c \
+	stacWrite.c
+
+sum_SOURCES = \
+	sum.c
+
+# Clean target
+clean-local:
+	-$(RM) -f TAGS gmon.* profile.txt
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
Index: /tags/rel-0_0_1/stac/src/calcGradient.c
===================================================================
--- /tags/rel-0_0_1/stac/src/calcGradient.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/calcGradient.c	(revision 7462)
@@ -0,0 +1,55 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "stac.h"
+
+int main(int argc, char *argv[])
+{
+    if (argc != 3) {
+        printf("Usage: %s FITS_IN FITS_OUT\n", argv[0]);
+        exit(EXIT_FAILURE);
+    }
+    const char *inName = argv[1];       // Input filename
+    const char *outName = argv[2];      // Output filename
+
+    psFits *inFile = psFitsOpen(inName, "r"); // File to read
+    psRegion imageRegion = {0, 0, 0, 0}; // Region of image to read
+
+    // We only read PHUs --- not mucking around with extensions for now
+    psImage *image = psFitsReadImage(NULL, inFile, imageRegion, 0);
+    if (image == NULL) {
+        psErrorStackPrint(stderr,"Fatal error: Unable to read %s\n", inName);
+        exit(EXIT_FAILURE);
+    }
+    psTrace("calcGradients.read", 4, "Image %s is %dx%d\n", inName, image->numCols, image->numRows);
+    // Convert to 32-bit floating point, in necessary
+    if (image->type.type != PS_TYPE_F32) {
+        psTrace("calcGradients.read", 3, "Converting %s to floating point in memory....", inName);
+        psImage *temp = psImageCopy(NULL, image, PS_TYPE_F32);
+        psFree(image);
+        image = temp;
+    } else {
+        int numNaN = psImageClipNaN(image, FLT_MAX);
+        if (numNaN) {
+            psTrace("calcGradients.read", 5, "Clipped %d NaN pixels.\n", numNaN);
+        }
+    }
+    psFitsClose(inFile);
+
+    psImage *grad = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+    for (int y = 0; y < image->numRows; y++) {
+        for (int x = 0; x < image->numCols; x++) {
+            grad->data.F32[y][x] = stacGradient(image, x, y);
+        }
+    }
+
+    psFree(image);
+
+    psFits *outFile = psFitsOpen(outName, "w");
+    if (!psFitsWriteImage(outFile, NULL, grad, 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", outName);
+    }
+    psFitsClose(outFile);
+
+    return EXIT_SUCCESS;
+}
+
Index: /tags/rel-0_0_1/stac/src/combine.c
===================================================================
--- /tags/rel-0_0_1/stac/src/combine.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/combine.c	(revision 7462)
@@ -0,0 +1,83 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "stac.h"
+#include "combineConfig.h"
+
+int main(int argc, char **argv)
+{
+    // Set trace levels
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("stac.checkMemory", 10);
+    psTraceSetLevel("stac.read", 10);
+    psTraceSetLevel("stac.scales", 7);
+    psTraceSetLevel("stac.rescale", 10);
+    psTraceSetLevel("stac.combine", 10);
+    psTraceSetLevel("combine", 10);
+
+    // Set logging level
+    psLogSetLevel(9);
+
+    // Parse command line
+    combineConfig *config = combineParseConfig(argc, argv); // Configuration
+
+    psArray *headers = NULL;            // Array of headers
+    psArray *images = stacReadImages(&headers, config->inNames); // The images
+    // Make fake errors
+    psArray *errors = psArrayAlloc(images->n);
+    for (int i = 0; i < images->n; i++) {
+        psImage *image = images->data[i];
+        psImage *err = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                err->data.F32[y][x] = 1.0;
+            }
+        }
+        errors->data[i] = err;
+    }
+
+    // Calculate scales between images
+    psVector *scales = NULL;            // Relative scales between images
+    psVector *offsets = NULL;           // Offsets between images
+    (void)stacScales(&scales, &offsets, images, config->starFile, config->starMap, 0.0, 0.0, config->aper);
+
+    // Set the saturation and bad values
+    psVector *saturated = psVectorAlloc(images->n, PS_TYPE_F32); // Saturation limits
+    psVector *bad = psVectorAlloc(images->n, PS_TYPE_F32); // Bad limits
+    for (int i = 0; i < images->n; i++) {
+        saturated->data.F32[i] = (config->saturated - offsets->data.F32[i]) / scales->data.F32[i];
+        bad->data.F32[i] = (config->bad - offsets->data.F32[i]) / scales->data.F32[i];
+    }
+
+    // Rescale the images
+    (void)stacRescale(images, errors, NULL, scales, offsets);
+
+    // Combine the images
+    psImage *combined = NULL;           // Combined image
+    psArray *rejected = NULL;           // Array of rejection masks
+    stacCombine(&combined, &rejected, images, errors, config->nReject, NULL, saturated, bad, config->reject);
+
+    psFits *outFile = psFitsOpen(config->outName, "r");
+    if (!psFitsWriteImage(outFile, headers->data[0], combined, 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", config->outName);
+    }
+    psTrace("combine", 1, "Combined image written to %s\n", config->outName);
+    psFitsClose(outFile);
+
+    // Clean up
+    psFree(headers);
+    psFree(combined);
+    psFree(rejected);
+    psFree(images);
+    psFree(errors);
+    psFree(saturated);
+    psFree(bad);
+    psFree(scales);
+    psFree(offsets);
+    combineConfigFree(config);
+
+#ifdef TESTING
+    // Check memory for leaks, corruption
+    stacCheckMemory();
+#endif
+
+}
Index: /tags/rel-0_0_1/stac/src/combineConfig.c
===================================================================
--- /tags/rel-0_0_1/stac/src/combineConfig.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/combineConfig.c	(revision 7462)
@@ -0,0 +1,161 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include "pslib.h"
+#include "combineConfig.h"
+
+void help(const char *programName
+    )
+{
+    fprintf (stderr, "shift: shift an image, given the transformation\n"
+	     "Usage: %s [-h] [-v] [-g GAIN] [-r READNOISE] [-s SAT] [-b BAD] [-p FILE MAP] [-a APER] [-k SIGMAREJ] [-n NREJECT] OUT IN1 IN2...\n"
+	     "where\n"
+	     "\t-h           Help (this info)\n"
+	     "\t-v           Increase verbosity level\n"
+	     "\t-g GAIN      Gain in e/ADU (1.0)\n"
+	     "\t-r READNOISE Read noise in e (0.0)\n"
+	     "\t-s SAT       Saturation point (65535)\n"
+	     "\t-b BAD       Bad level (0)\n"
+	     "\t-p FILE MAP  Specify file containing star coordinates, with map\n"
+	     "\t-a APER      Aperture radius for photometry (3.0)\n"
+	     "\t-k SIGMAREJ  k-sigma rejection threshold (3.0)\n"
+	     "\t-n NREJECT   Number of rejection iterations (1)\n"
+	     "\tOUT          Output image\n"
+	     "\tIN1 IN2...   Input images (identical size)\n",
+	     programName
+	);
+}
+
+
+combineConfig *combineConfigAlloc(void)
+{
+    combineConfig *config = psAlloc(sizeof(combineConfig)); // Configuration
+
+    // Parameters with default values
+    config->verbose = 0;		// Verbosity level
+    config->gain = 1.0;			// Gain (e/ADU)
+    config->readnoise = 0.0;		// Read noise (e)
+    config->reject = 4.0;		// Rejection threshold (sigma)
+    config->nReject = 1;		// Number of rejection iterations
+    config->saturated = 65535.0;	// Saturation level
+    config->bad = 0.0;			// Bad level
+    config->outName = NULL;		// Output name
+    config->inNames = NULL;		// Input names;
+    config->starFile = NULL;		// Filename of file containing stars
+    config->starMap = NULL;		// Map for stars
+    config->aper = 3.0;			// Aperture for photometry
+
+    return config;
+}
+
+void combineConfigFree(combineConfig *config)
+{
+    psFree(config->inNames);
+    psFree(config);
+}
+
+
+combineConfig *combineParseConfig(int argc, char *argv[])
+{
+    combineConfig *config = combineConfigAlloc(); // Configuration
+
+    const char *programName = argv[0];	// Program name
+
+    /* Variables for getopt */
+    int opt;   /* Option, from getopt */
+    extern char *optarg;   /* Argument accompanying switch */
+    extern int optind;   /* getopt variables */
+
+    /* Parse command-line arguments using getopt */
+    while ((opt = getopt(argc, argv, "hvg:r:s:b:p:a:k:n:")) != -1) {
+        switch (opt) {
+	  case 'h':
+	    help(programName);
+	    exit(EXIT_SUCCESS);
+	  case 'v':
+            config->verbose++;
+            break;
+	  case 'r':
+            if (sscanf(optarg, "%f", &config->readnoise) != 1) {
+		printf("Unable to read readnoise.\n");
+                help(programName);
+                exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'g':
+	    if (sscanf(optarg, "%f", &config->gain) != 1) {
+ 		printf("Unable to read gain.\n");
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 's':
+            if (sscanf(optarg, "%f", &config->saturated) != 1) {
+ 		printf("Unable to read saturation limit.\n");
+                help(programName);
+                exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'b':
+            if (sscanf(optarg, "%f", &config->bad) != 1) {
+ 		printf("Unable to read bad limit.\n");
+		help(programName);
+                exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'p':
+	    if (argc < optind+1) {
+ 		printf("Unable to read photometric files.\n");
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    config->starFile = argv[optind-1];
+	    config->starMap = argv[optind++];
+	    // Note: incrementing optind, so I can read more than one parameter.
+	    break;
+	  case 'a':
+            if (sscanf(optarg, "%f", &config->aper) != 1) {
+ 		printf("Unable to read aperture.\n");
+                help(programName);
+                exit(EXIT_FAILURE);
+            }
+	    break;
+	  case 'k':
+            if (sscanf(optarg, "%f", &config->reject) != 1) {
+ 		printf("Unable to read rejection limit.\n");
+                help(programName);
+                exit(EXIT_FAILURE);
+            }
+	    break;
+	  case 'n':
+            if (sscanf(optarg, "%d", &config->nReject) != 1) {
+  		printf("Unable to read number of rejection iterations.\n");
+		help(programName);
+                exit(EXIT_FAILURE);
+            }
+	    break;
+	  default:
+	    printf("Bad option: %c\n", opt);
+	    help(programName);
+	    exit(EXIT_FAILURE);
+	}
+    }
+
+    /* Get the remaining, mandatory values */
+    argc -= optind;
+    argv += optind;
+    if (argc < 2) {
+        help(programName);
+        exit(EXIT_FAILURE);
+    }
+    config->outName = argv[0];		// Output filename
+    config->inNames = psArrayAlloc(argc-1); // Input filenames
+    for (int i = 1; i < argc; i++) {
+	config->inNames->data[i-1] = psAlloc(strlen(argv[i]));
+	strncpy(config->inNames->data[i-1], argv[i], strlen(argv[i]));
+    }
+
+    return config;
+}
Index: /tags/rel-0_0_1/stac/src/combineConfig.h
===================================================================
--- /tags/rel-0_0_1/stac/src/combineConfig.h	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/combineConfig.h	(revision 7462)
@@ -0,0 +1,28 @@
+#ifndef COMBINE_CONFIG_H
+#define COMBINE_CONFIG_H
+
+typedef struct {
+    int verbose;			// Verbosity level
+    float gain;				// Gain (e/ADU)
+    float readnoise;			// Read noise (e)
+    float reject;			// Rejection threshold (sigma)
+    int nReject;			// Number of rejection iterations
+    float saturated;			// Saturation level
+    float bad;				// Bad level
+    char *outName;			// Output name
+    psArray *inNames;			// Input names;
+    char *starFile;			// Filename of file containing stars
+    char *starMap;			// Map for stars
+    float aper;				// Aperture for photometry
+} combineConfig;
+
+
+combineConfig *combineConfigAlloc(void);// Allocator
+void combineConfigFree(combineConfig *config); // Deallocator
+
+void help(const char *programName);	// Print help
+combineConfig *combineParseConfig(int argc, char *argv[]); // Parse command line
+
+
+
+#endif
Index: /tags/rel-0_0_1/stac/src/shift.c
===================================================================
--- /tags/rel-0_0_1/stac/src/shift.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/shift.c	(revision 7462)
@@ -0,0 +1,175 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include "pslib.h"
+#include "stac.h"
+
+void help(const char *programName
+    )
+{
+    fprintf (stderr, "shift: shift an image, given the transformation\n"
+             "Usage: %s [-h] [-v] [-s NX NY] IN OUT\n"
+             "where\n"
+             "\t-h           Help (this info)\n"
+             "\t-v           Increase verbosity level\n"
+             "\t-s NX NY     Size of output image\n"
+             "\tIN           Input image, which has associated .map file\n"
+             "\tOUT          Output image\n",
+             programName
+        );
+}
+
+
+int main(int argc, char **argv)
+{
+    // Set trace levels
+    psTraceSetLevel(".", 0);
+    psTraceSetLevel("stac.checkMemory", 10);
+    psTraceSetLevel("stac.read", 10);
+    psTraceSetLevel("stac.invertMaps", 10);
+    psTraceSetLevel("stac.transform", 10);
+    psTraceSetLevel("stac.size", 10);
+    psTraceSetLevel("shift", 10);
+
+    // Set logging level
+    psLogSetLevel(9);
+
+    // Parameters with default values
+    int outnx = 0, outny = 0;           // Size of output image
+    int verbose = 0;                    // Verbosity level
+    float bad = 0.0;                    // Bad level
+
+    // Parse command line
+    const char *programName = argv[0];  // Program name
+    /* Variables for getopt */
+    int opt;   /* Option, from getopt */
+    extern char *optarg;   /* Argument accompanying switch */
+    extern int optind;   /* getopt variables */
+
+    /* Parse command-line arguments using getopt */
+    while ((opt = getopt(argc, argv, "hvb:s:")) != -1) {
+        switch (opt) {
+          case 'h':
+            help(programName);
+            exit(EXIT_SUCCESS);
+          case 'v':
+            verbose++;
+            break;
+          case 's':
+            if ((sscanf(argv[optind-1], "%d", &outnx) != 1) || (sscanf(argv[optind++], "%d", &outny) != 1)) {
+                // Note: incrementing optind, so I can read more than one parameter.
+                help(programName);
+                exit(EXIT_FAILURE);
+            }
+            break;
+          case 'b':
+            if (sscanf(optarg, "%f", &bad) != 1) {
+                help(programName);
+                exit(EXIT_FAILURE);
+            }
+            break;
+          default:
+            help(programName);
+            exit(EXIT_FAILURE);
+        }
+    }
+
+    /* Get the remaining, mandatory values */
+    argc -= optind;
+    argv += optind;
+    if (argc < 2) {
+        help(programName);
+        exit(EXIT_FAILURE);
+    }
+    const char *inName = argv[0];       // Input filename
+    const char *outName = argv[1];      // Output filename
+
+    psFits *imageFile = psFitsOpen(inName, "r");
+    psRegion imageRegion = {0, 0, 0, 0}; // Region of image to read
+    psMetadata *header = psFitsReadHeader(NULL, imageFile); // FITS header
+    psImage *image = psFitsReadImage(NULL, imageFile, imageRegion, 0);
+    if (image == NULL) {
+        psErrorStackPrint(stderr, "Fatal error: Unable to read %s\n", inName);
+        exit(EXIT_FAILURE);
+    }
+    psTrace("shift", 4, "Image %s is %dx%d\n", inName, image->numCols, image->numRows);
+    // Convert to 32-bit floating point, in necessary
+    if (image->type.type != PS_TYPE_F32) {
+        psTrace("shift", 5, "Converting %s to floating point in memory....", inName);
+        psImage *temp = psImageCopy(NULL, image, PS_TYPE_F32);
+        psFree(image);
+        image = temp;
+    }
+    psFitsClose(imageFile);
+
+    // Generate masks
+    psImage *mask = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+    for (int y = 0; y < image->numRows; y++) {
+        for (int x = 0; x < image->numCols; x++) {
+            if (image->data.F32[y][x] <= bad) {
+                mask->data.U8[y][x] = 1;
+            } else {
+                mask->data.U8[y][x] = 0;
+            }
+        }
+    }
+
+    // Read map
+    char mapName[MAXCHAR];              // Name of map file
+    sprintf(mapName, "%s.map", inName);
+    psPlaneTransform *map = stacReadMap(mapName);
+
+    // Functions work on array of images, so:
+    psArray *images = psArrayAlloc(1);  // An array of one
+    psArray *maps = psArrayAlloc(1);    // An array of one
+    psArray *masks = psArrayAlloc(1);   // An array of one
+    images->data[0] = image;
+    maps->data[0] = map;
+    masks->data[0] = mask;
+
+    // Get size
+    if (outnx == 0 || outny == 0) {
+        stacSize(&outnx, &outny, NULL, NULL, images, maps);
+    }
+
+    // Invert maps
+    psArray *inverseMaps = stacInvertMaps(maps, outnx, outny);
+
+    // Transform inputs and errors
+    psArray *transformed = NULL;
+    (void)stacTransform(&transformed, NULL, images, inverseMaps, NULL, masks, NULL, NULL, NULL, outnx, outny);
+
+    // Update FITS header appropriately
+    psMetadataAddS32(header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Number of pixels in x",
+                     (int)((psImage*)transformed->data[0])->numCols);
+    psMetadataAddS32(header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Number of pixels in y",
+                     (int)((psImage*)transformed->data[0])->numRows);
+    psMetadataAddS32(header, PS_LIST_TAIL, "BITPIX", PS_META_REPLACE, "Bits per pixel", -32);
+
+    // Write out transformed image
+    psFits *outFile = psFitsOpen(outName, "w");
+    int numPix = psImageClipNaN(transformed->data[0], 0.0);
+    if (numPix > 0) {
+        psTrace("stac", 3, "Clipping %d NaN pixels to zero.\n", numPix);
+    }
+    if (!psFitsWriteImage(outFile, header, transformed->data[0], 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", outName);
+    }
+    psTrace("shift", 1, "Transformed image written to %s\n", outName);
+    psFitsClose(outFile);
+
+    // Free everything I've used
+    psFree(images);
+    psFree(maps);
+    psFree(masks);
+    psFree(inverseMaps);
+    psFree(transformed);
+
+#ifdef TESTING
+    // Check memory for leaks, corruption
+    stacCheckMemory();
+#endif
+
+}
Index: /tags/rel-0_0_1/stac/src/shiftSize.c
===================================================================
--- /tags/rel-0_0_1/stac/src/shiftSize.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/shiftSize.c	(revision 7462)
@@ -0,0 +1,105 @@
+/*
+ * shiftSize:
+ * A stripped-down version of STAC that outputs the necessary size to keep all the pixels in the individual
+ * images within the shifted images.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include "pslib.h"
+#include "stac.h"
+
+void help(const char *name)
+{
+    fprintf (stderr, "shiftSize: Calculate size for output combined image\n"
+	     "Usage: %s [-h] [-v] IN1 IN2...\n"
+	     "where\n"
+	     "\t-h           Help (this info)\n"
+	     "\t-v           Increase verbosity level\n"
+	     "\tIN1, IN2...  Input images, which have associated .map files.\n",
+	     name
+	);
+}
+
+
+int main(int argc, char *argv[])
+{
+    // Set trace levels
+    psTraceSetLevel(".",0);
+#ifdef TESTING
+    psTraceSetLevel("stac.checkMemory",10);
+    psTraceSetLevel("stac.read",10);
+    psTraceSetLevel("stac.size",10);
+#endif
+
+    // Set logging level
+    psLogSetLevel(9);
+
+
+    int verbose = 0;			// Verbosity level
+
+    // Parse command line
+    const char *programName = argv[0];	// Program name
+    /* Variables for getopt */
+    int opt;   /* Option, from getopt */
+    extern char *optarg;   /* Argument accompanying switch */
+    extern int optind;   /* getopt variables */
+
+    /* Parse command-line arguments using getopt */
+    while ((opt = getopt(argc, argv, "hv")) != -1) {
+        switch (opt) {
+	  case 'h':
+	    help(programName);
+	    exit(EXIT_SUCCESS);
+	  case 'v':
+            verbose++;
+            break;
+	  default:
+	    help(programName);
+	}
+    }
+
+    /* Get the remaining, mandatory values */
+    argc -= optind;
+    argv += optind;
+    if (argc < 1) {
+        help(programName);
+        exit(EXIT_FAILURE);
+    }
+
+    psArray *inputs = psArrayAlloc(argc); // Input filenames
+    for (int i = 0; i < argc; i++) {
+	inputs->data[i] = psAlloc(strlen(argv[i]));
+	strncpy(inputs->data[i], argv[i], strlen(argv[i]));
+    }
+
+    // Read input files
+    psArray *images = stacReadImages(NULL, inputs);
+
+    // Read maps
+    psArray *maps = stacReadMaps(inputs);
+
+    // Get size
+    int outnx, outny;			// Size of combined image
+    float xMapDiff, yMapDiff;		// Difference to apply to maps
+    stacSize(&outnx, &outny, &xMapDiff, &yMapDiff, images, maps);
+
+    printf("%d %d\n", outnx, outny);
+
+    // Write out altered maps
+    stacWriteMaps(inputs, maps);
+
+    psFree(inputs);
+    psFree(images);
+    psFree(maps);
+
+#ifdef TESTING
+    // Check memory for leaks, corruption
+    stacCheckMemory();
+#endif
+
+}
Index: /tags/rel-0_0_1/stac/src/stac.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stac.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stac.c	(revision 7462)
@@ -0,0 +1,300 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "stac.h"
+#include "stacConfig.h"
+#include <sys/time.h>
+
+int main(int argc, char **argv)
+{
+    double startTime;
+    startTime = getTime();
+
+#if 0
+    psMemAllocateCallbackSet(stacMemPrint);
+    psMemAllocateCallbackSetID(185);
+    psMemFreeCallbackSet(stacMemPrint);
+    psMemFreeCallbackSetID(185);
+#endif
+
+    // Set trace levels
+    psTraceSetLevel(".",0);
+    psTraceSetLevel("stac",10);
+    psTraceSetLevel("stac.checkMemory",10);
+    psTraceSetLevel("stac.config",10);
+    psTraceSetLevel("stac.read",10);
+    psTraceSetLevel("stac.invertMaps",10);
+    psTraceSetLevel("stac.errors",10);
+    psTraceSetLevel("stac.transform",10);
+    psTraceSetLevel("stac.combine",10);
+    psTraceSetLevel("stac.rejection",10);
+    psTraceSetLevel("stac.time",10);
+    psTraceSetLevel("stac.area",10);
+    psTraceSetLevel("stac.size",10);
+    psTraceSetLevel("stac.scales",7);
+
+    // Set logging level
+    psLogSetLevel(9);
+
+    // Parse command line
+    stacConfig *config = stacParseConfig(argc, argv);
+
+    // Read input files
+    psArray *headers = NULL;            // Array of headers
+    psArray *inputs = stacReadImages(&headers, config->inputs);
+
+    // Generate masks
+    psArray *masks = psArrayAlloc(inputs->n);
+    for (int i = 0; i < inputs->n; i++) {
+        psImage *image = inputs->data[i]; // Image for which to get mask
+        psImage *mask = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                if (image->data.F32[y][x] <= config->bad) {
+                    mask->data.U8[y][x] = 1;
+                } else {
+                    mask->data.U8[y][x] = 0;
+                }
+            }
+        }
+        masks->data[i] = mask;
+    }
+
+    // Read maps
+    psArray *maps = stacReadMaps(config->inputs);
+
+    psTrace("stac.time", 1, "I/O completed at %f seconds\n", getTime() - startTime);
+
+    // Get size, if not input
+    if (config->outnx == 0 || config->outny == 0) {
+        stacSize(&config->outnx, &config->outny, &config->xMapDiff, &config->yMapDiff, inputs, maps);
+    }
+
+    // Fix up the headers
+    for (int i = 0; i < headers->n; i++) {
+        psMetadata *header = headers->data[i];
+        psMetadataAddS32(header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Number of pixels in x",
+                         config->outnx);
+        psMetadataAddS32(header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Number of pixels in y",
+                         (int)config->outny);
+        psMetadataAddS32(header, PS_LIST_TAIL, "BITPIX", PS_META_REPLACE, "Bits per pixel", -32);
+#if 0
+        bool mdok = true;               // Result of MD lookup
+        float crpix1 = psMetadataLookupF32(&mdok, header, "CRPIX1");
+        if (mdok && !isnan(crpix1)) {
+            psMetadataAddF32(header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE,
+                             "WCS Coordinate reference pixel", crpix1 -
+#endif
+    }
+
+    // Invert maps
+    psArray *inverseMaps = stacInvertMaps(maps, config->outnx, config->outny);
+
+    // Generate errors
+    psArray *errors = stacErrorImages(inputs, config->gain, config->readnoise);
+#ifdef TESTING
+    // Write error images out to check
+    for (int i = 0; i < inputs->n; i++) {
+        char errName[MAXCHAR];          // Filename of error image
+        sprintf(errName,"%s.err",config->inputs->data[i]);
+        psFits *errorFile = psFitsOpen(errName, "w");
+        if (!psFitsWriteImage(errorFile, NULL, errors->data[i], 0)) {
+            psErrorStackPrint(stderr, "Unable to write image: %s\n", errName);
+        }
+        psTrace("stac", 1, "Error image written to %s\n", errName);
+        psFitsClose(errorFile);
+    }
+#endif
+
+    // Transform inputs and errors
+    psArray *transformed = NULL;
+    psArray *transformedErrors = NULL;
+    (void)stacTransform(&transformed, &transformedErrors, inputs, inverseMaps, errors, masks, NULL, NULL, NULL,
+                        config->outnx, config->outny);
+    psTrace("stac.time",1,"Transformation completed at %f seconds\n", getTime() - startTime);
+
+#ifdef TESTING
+    // Write transformed images out to check
+    for (int i = 0; i < inputs->n; i++) {
+        char shiftName[MAXCHAR];        // Filename of shift image
+        char errName[MAXCHAR];          // Filename of error image
+        sprintf(shiftName,"%s.shift1",config->inputs->data[i]);
+        sprintf(errName,"%s.shifterr1",config->inputs->data[i]);
+        psFits *shiftFile = psFitsOpen(shiftName, "w");
+        psFits *errFile = psFitsOpen(errName, "w");
+        psImage *trans = transformed->data[i]; // Transformed image
+        psImage *transErr = transformedErrors->data[i]; // Transformed error image
+        if (!psFitsWriteImage(shiftFile, NULL, trans, 0)) {
+            psErrorStackPrint(stderr, "Unable to write image: %s\n", shiftName);
+        }
+        psTrace("stac", 1, "Shifted image written to %s\n", shiftName);
+        if (!psFitsWriteImage(errFile, NULL, transErr, 0)) {
+            psErrorStackPrint(stderr, "Unable to write image: %s\n", errName);
+        }
+        psTrace("stac", 1, "Shifted error image written to %s\n", errName);
+        psFitsClose(shiftFile);
+        psFitsClose(errFile);
+    }
+#endif
+
+    psVector *scales = NULL;            // Relative scales between images
+    psVector *offsets = NULL;           // Offsets between images
+    (void)stacScales(&scales, &offsets, transformed, config->starFile, config->starMapFile, config->xMapDiff,
+                     config->yMapDiff, config->aper);
+    // Rescale the images
+    (void)stacRescale(transformed, transformedErrors, NULL, scales, offsets);
+    // Set the saturation and bad values
+    psVector *saturated = psVectorAlloc(transformed->n, PS_TYPE_F32); // Saturation limits
+    psVector *bad = psVectorAlloc(transformed->n, PS_TYPE_F32); // Bad limits
+    for (int i = 0; i < transformed->n; i++) {
+        saturated->data.F32[i] = (config->saturated - offsets->data.F32[i]) / scales->data.F32[i];
+        bad->data.F32[i] = (config->bad - offsets->data.F32[i]) / scales->data.F32[i];
+    }
+
+    // Save shifted images
+    if (config->saveShifts) {
+        psImage *image = NULL;          // Copy of image, to remove NAN for output
+        for (int i = 0; i < transformed->n; i++) {
+            char shiftName[MAXCHAR];    // Filename of shifted image
+            sprintf(shiftName, "%s.shift", (const char*)config->inputs->data[i]);
+            psFits *shiftFile = psFitsOpen(shiftName, "w");
+            image = psImageCopy(NULL, transformed->data[i], PS_TYPE_F32);
+            (void)psImageClipNaN(image, 0.0);
+            if (!psFitsWriteImage(shiftFile, headers->data[i], image, 0)) {
+                psErrorStackPrint(stderr, "Unable to write image: %s\n", shiftName);
+            }
+            psTrace("stac", 1, "Shifted image %d written to %s\n", i, shiftName);
+            psFitsClose(shiftFile);
+            psFree(image);
+        }
+    }
+
+    // Combine with rejection
+    psArray *rejected = NULL;
+    psImage *combined = NULL;
+    (void)stacCombine(&combined, &rejected, transformed, transformedErrors, config->nReject, NULL, saturated,
+                      bad, config->reject);
+
+    psTrace("stac.time",1,"First combination completed at %f seconds\n", getTime() - startTime);
+
+
+#ifdef TESTING
+    // Write rejection images out to check
+    for (int i = 0; i < rejected->n; i++) {
+        char rejName[MAXCHAR];  // Filename of rejection image
+        sprintf(rejName, "%s.shiftrej", config->inputs->data[i]);
+
+        psFits *rejFile = psFitsOpen(rejName, "w");
+        if (!psFitsWriteImage(rejFile, NULL, rejected->data[i], 0)) {
+            psErrorStackPrint(stderr, "Unable to write image: %s\n", rejName);
+        }
+        psTrace("stac", 1, "Rejection image written to %s\n", rejName);
+        psFitsClose(rejFile);
+    }
+
+    // Write out pre-combined image
+    char preName[MAXCHAR];              // Filename of precombined image
+    sprintf(preName, "%s.pre", config->output);
+    psFits *preFile = psFitsOpen(preName, "w");
+    if (!psFitsWriteImage(preFile, NULL, combined, 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", preName);
+    }
+    psTrace("stac", 1, "Pre-combined image written to %s\n", preName);
+    psFitsClose(preFile);
+#endif
+
+    // Get regions of interest in the source frame
+    psArray *regions = psArrayAlloc(inputs->n); // Array of images denoting regions of interest
+    for (int i = 0; i < inputs->n; i++) {
+        regions->data[i] = stacAreaOfInterest(rejected->data[i], inverseMaps->data[i],
+                                              ((psImage*)(inputs->data[i]))->numCols,
+                                              ((psImage*)(inputs->data[i]))->numRows);
+#ifdef TESTING
+        char regionName[MAXCHAR];       // Filename of region image
+        sprintf(regionName,"%s.region",config->inputs->data[i]);
+        psFits *regionFile = psFitsOpen(regionName, "w");
+        if (!psFitsWriteImage(regionFile, NULL, regions->data[i], 0)) {
+            psErrorStackPrint(stderr, "Unable to write image: %s\n", regionName);
+        }
+        psTrace("stac", 1, "Region image written to %s\n", regionName);
+        psFitsClose(regionFile);
+#endif
+    }
+
+    // Transform rejected pixels to source frame
+    psArray *rejectedSource = stacRejection(inputs, rejected, regions, maps, inverseMaps, config->frac,
+                                            config->grad, config->inputs);
+
+    // Get regions of interest in the output frame
+    psImage *combineRegion = psImageAlloc(config->outnx, config->outny, PS_TYPE_U8);
+    for (int y = 0; y < config->outny; y++) {
+        for (int x = 0; x < config->outnx; x++) {
+            combineRegion->data.U8[y][x] = 0;
+        }
+    }
+    for (int i = 0; i < inputs->n; i++) {
+        psImage *region = stacAreaOfInterest(rejectedSource->data[i], maps->data[i], config->outnx,
+                                             config->outny);
+        psBinaryOp(combineRegion, combineRegion, "+", region);
+        psFree(region);
+
+        // Do OR of masks
+        psImage *mask = masks->data[i]; // Mask of input image (bad columns etc)
+        psImage *cr = rejectedSource->data[i]; // Mask of CRs
+        for (int y = 0; y < mask->numRows; y++) {
+            for (int x = 0; x < mask->numCols; x++) {
+                if (cr->data.U8[y][x] > 0) {
+                    mask->data.U8[y][x] = 1;
+                }
+            }
+        }
+    }
+
+    // Redo transformation with the masks and scales/offsets
+    (void)stacTransform(&transformed, &transformedErrors, inputs, inverseMaps, errors, masks,
+                        combineRegion, scales, offsets, config->outnx, config->outny);
+
+    // Combine the newly-transformed CR-free images, no rejection
+    psFree(rejected);
+    rejected = NULL;
+    (void)stacCombine(&combined, &rejected, transformed, transformedErrors, 0, combineRegion, saturated,
+                      bad, config->reject);
+
+    // Write output image
+    psFits *outFile = psFitsOpen(config->output, "w");
+    int numPix = psImageClipNaN(combined, 0.0);
+    if (numPix > 0) {
+        psTrace("stac", 3, "Clipping %d NaN pixels to zero.\n", numPix);
+    }
+    if (!psFitsWriteImage(outFile, headers->data[0], combined, 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", config->output);
+    }
+    psTrace("stac", 1, "Combined image written to %s\n", config->output);
+    psFitsClose(outFile);
+
+    // Free everything I've used
+    stacConfigFree(config);
+    psFree(scales);
+    psFree(offsets);
+    psFree(combineRegion);
+    psFree(regions);
+    psFree(inputs);
+    psFree(maps);
+    psFree(inverseMaps);
+    psFree(errors);
+    psFree(masks);
+    psFree(transformedErrors);
+    psFree(transformed);
+    psFree(rejectedSource);
+    psFree(combined);
+    psFree(saturated);
+    psFree(bad);
+
+    psTrace("stac.time",1,"Final combination completed at %f seconds\n", getTime() - startTime);
+
+#ifdef TESTING
+    // Check memory for leaks, corruption
+    stacCheckMemory();
+#endif
+
+    exit(EXIT_SUCCESS);
+}
Index: /tags/rel-0_0_1/stac/src/stac.h
===================================================================
--- /tags/rel-0_0_1/stac/src/stac.h	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stac.h	(revision 7462)
@@ -0,0 +1,209 @@
+#ifndef STAC_H
+#define STAC_H
+
+#include "pslib.h"
+#define abs(x) ((x) >= 0 ? (x) : (-(x)))
+#define MAXCHAR 80
+
+/************************************************************************************************************/
+
+// time.c
+
+// Get the current time
+double getTime(void);
+
+/************************************************************************************************************/
+
+// stacRead.c
+
+// Read the input files and return an array of images
+psArray *stacReadImages(psArray **headers, // The image headers, to be returned
+			psArray *filenames // The file names of the images
+    );
+
+// Read a file of coordinates (x,y)
+psArray *stacReadCoords(const char *filename);
+
+// Read a single map file and return the transformation
+psPlaneTransform *stacReadMap(const char *filename);
+
+// Read the map files and return an array of transformations
+psArray *stacReadMaps(psArray *filenames // The file names of the images whose maps are to be read
+    );
+
+/************************************************************************************************************/
+
+// stacErrorImages.c
+
+// Calculate the error images
+psArray *stacErrorImages(psArray *inputs, // Array of input images
+			 float gain,	// Gain, in e/ADU
+			 float rn	// Read noise, in e
+    );
+
+/************************************************************************************************************/
+
+// stacTransform.c
+
+// Transform input images, return success
+bool stacTransform(psArray **outputs,	// Transformed images for output
+		   psArray **outErrors, // Transformed error images for output
+		   const psArray *images, // Array of images to be transformed
+		   const psArray *maps, // Array of polynomials that do the transformation
+		   const psArray *errors, // Array of error images to be transformed
+		   const psArray *masks, // Masks of input images
+		   const psImage *region, // Region of interest for transformation
+		   const psVector *scales, // Relative scales
+		   const psVector *offsets, // Relative offsets
+		   int outnx, int outny	// Size of output images
+    );
+
+/************************************************************************************************************/
+
+// stacCheckMemory.c
+
+// Check memory
+void stacCheckMemory(void);
+
+// Print out the problem
+void stacMemoryProblem(const psMemBlock* ptr, ///< the pointer to the problematic memory block.
+		       const char *file, ///< the file in which the problem originated
+		       psS32 lineno	///< the line number in which the problem originated
+    );
+
+// Print out a memblock when it's allocated --- this function used as a callback
+psMemId stacMemPrint(const psMemBlock *ptr);
+
+
+/************************************************************************************************************/
+
+// stacCombine.c
+
+// Get the mean for a bunch of values
+float stacCombineMean(psVector *values,	// Values for which to take the mean
+		      psVector *errors,	// Errors in the values
+		      psVector *masks	// Masks for the values, 0 = don't use, 1 = use
+    );
+
+// Get the median for a bunch of values
+float stacCombineMedian(psVector *values, // Values for which to take the median
+			psVector *errors, // Errors in the values
+			psVector *masks	// Masks for the values, 0 = don't use, 1 = use
+    );
+
+// Combine the transformed images
+bool stacCombine(psImage **combined,	// The combined image for output
+		 psArray **rejected,	// Array of rejection masks
+		 psArray *images,	// Array of transformed images
+		 psArray *errors,	// Array of transformed error images
+		 int nReject,		// Number of rejection iterations
+		 psImage *region,	// Region to combine
+		 psVector *saturated,	// Saturation limits for each image
+		 psVector *bad,		// Bad pixel limits for each image
+		 float reject		// Rejection (k-sigma)
+    );
+
+/************************************************************************************************************/
+
+// stacInvertMaps.c
+
+// Invert an array of maps
+psArray *stacInvertMaps(const psArray *maps, // Array of maps to invert
+			int outnx, int outny // Size of output image
+    );
+
+/************************************************************************************************************/
+
+// stacRejection.c
+
+// Return the relative gradient for a given pixel
+float stacGradient(psImage *image,	// Input for which to measure the gradient
+		   int x, int y		// Coordinates at which to measure the gradient
+    );
+
+// Transform the rejection masks back to the source frame
+psArray *stacRejection(psArray *inputs,	// Input images
+		       psArray *rejected, // Rejected images
+		       psArray *regions, // Regions of interest
+		       psArray *maps,	// Maps from input to transformed image
+		       psArray *inverseMaps, // Maps from transformed to input image
+		       float frac,	// Fraction of pixel rejected before looking more carefully
+		       float grad,	// Gradient limit for rejection
+		       psArray *names	// Names of original images (only for writing out when TESTING)
+    );
+
+/************************************************************************************************************/
+
+// stacAreaOfInterest.c
+
+// Returns the change in x' and y' for a change in x and y of 1
+bool stacPlaneTransformDeriv(psPlane *out, // Output derivative, assumed already allocated
+			     psPlaneTransform *transform, // The transform for which to obtain the derivative
+			     psPlane *in // The position at which to obtain the derivative
+    );
+
+// Return a mask image designating the region of interest in the source frame
+// Basically, uses derivatives of the transformation to work out what pixels in the source might be of interest
+psImage *stacAreaOfInterest(psImage *mask, // Mask that is to be used to determine the area of interest
+			    psPlaneTransform *map, // Map from the mask to the area of interest
+			    int nxOut, int nyOut // Size of the area of interest
+    );
+
+/************************************************************************************************************/
+
+// stacSize.c
+
+// Calculate the size of the output image
+bool stacSize(int *outnx, int *outny,	// Size of output image (returned)
+	      float *xMapDiff, float *yMapDiff, // Difference applied to maps
+	      const psArray *images,	// Input images
+	      psArray *maps		// Transformation maps
+    );
+
+/************************************************************************************************************/
+
+// stacScale.c
+
+// Calculate the background in an image
+float stacBackground(const psImage *image, // Image for which to get the background
+		     int sample		// Sample in increments of this value
+    );
+
+
+// Calculate the relative scales and offsets between the images
+bool stacScales(psVector **scalesPtr,	// Scales to return
+		psVector **offsetsPtr,	// Offsets to return
+		const psArray *images,	// Images on which to measure the scales and offsets
+		const char *starFile,	// File containing coordinates to photometer
+		const char *starMapFile, // Map for coodinates to the common output frame
+		float xMapDiff, float yMapDiff, // Difference from the map to apply (due to size difference)
+		float aper		// Aperture to use for photometry (radius)
+    );
+
+// Rescale images
+bool stacRescale(psArray *images,	// Images to rescale
+		 psArray *errImages,	// Variance images to rescale
+		 const psImage *mask,	// Mask indicating which pixels to scale
+		 const psVector *scales,// Scales for images
+		 const psVector *offsets // Offsets for images
+    );
+
+
+
+/************************************************************************************************************/
+
+// stacWrite.c
+
+// Write map out
+bool stacWriteMap(const char *mapName,	// Filename to write to
+		  psPlaneTransform *map	// Map to write
+    );
+
+// Write multiple maps
+bool stacWriteMaps(const psArray *names, // Filenames of the input images (will add ".map")
+		   const psArray *maps	// Maps to write
+    );
+
+/************************************************************************************************************/
+
+#endif
Index: /tags/rel-0_0_1/stac/src/stacAreaOfInterest.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacAreaOfInterest.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacAreaOfInterest.c	(revision 7462)
@@ -0,0 +1,118 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+#define MAX(a,b) ((a) > (b) ? (a) : (b))
+#define MIN(a,b) ((a) < (b) ? (a) : (b))
+
+// Returns the change in x' and y' for a change in x and y of 1
+bool stacPlaneTransformDeriv(psPlane *out, // Output derivative, assumed already allocated
+			     psPlaneTransform *transform, // The transform for which to obtain the derivative
+			     psPlane *in // The position at which to obtain the derivative
+    )
+{
+    psPolynomial2D *xPoly = transform->x;
+    psPolynomial2D *yPoly = transform->y;
+
+    assert(xPoly->type == PS_POLYNOMIAL_ORD);
+    assert(yPoly->type == PS_POLYNOMIAL_ORD);
+
+    // Initialise derivatives
+    out->x = 0.0;
+    out->y = 0.0;
+
+    // Do x component
+    double xSum = 1.0 / in->x;		// 1/x in order to calculate x^(i-1)
+    for (int xOrder = 0; xOrder < xPoly->nX; xOrder++) {
+        double ySum = xSum / in->y;	// 1/y in order to calculate y^(j-1)
+        for (int yOrder = 0; yOrder < xPoly->nY; yOrder++) {
+            if (xPoly->mask[xOrder][yOrder] == 0) {
+		out->x += xPoly->coeff[xOrder][yOrder] * (double)xOrder * ySum * in->y;	// a_ij i x^(i-1) y^j
+		out->x += xPoly->coeff[xOrder][yOrder] * (double)yOrder * ySum * in->x; // a_ij j x^i y^(j-1)
+            }
+	    ySum *= in->y;
+        }
+	xSum *= in->x;
+    }
+
+    // Do y component
+    xSum = 1.0 / in->x;			// 1/x in order to calculate x^(i-1)
+    for (int xOrder = 0; xOrder < yPoly->nX; xOrder++) {
+        double ySum = xSum / in->y;	// 1/y in order to calculate y^(j-1)
+        for (int yOrder = 0; yOrder < yPoly->nY; yOrder++) {
+            if (yPoly->mask[xOrder][yOrder] == 0) {
+		out->y += yPoly->coeff[xOrder][yOrder] * (float)xOrder * ySum * in->y;	// a_ij i x^(i-1) y^j
+		out->y += yPoly->coeff[xOrder][yOrder] * (float)yOrder * ySum * in->x; // a_ij j x^i y^(j-1)
+            }
+	    ySum *= in->y;
+        }
+	xSum *= in->x;
+    }
+
+    return true;
+}
+
+
+
+// Calculate 
+psImage *stacAreaOfInterest(psImage *mask, // Mask that is to be used to determine the area of interest
+			    psPlaneTransform *map, // Map from the mask to the area of interest
+			    int nxOut, int nyOut // Size of the area of interest
+    )
+{
+    // Input checks
+    assert(mask);
+    assert(map);
+    assert(mask->type.type == PS_TYPE_U8);
+    
+    // Number of pixels in x and y
+    int nxIn = mask->numCols;
+    int nyIn = mask->numRows;
+
+    // Create and zero output image
+    psImage *output = psImageAlloc(nxOut, nyOut, PS_TYPE_U8);
+    for (int y = 0; y < nyOut; y++) {
+	for (int x = 0; x < nxOut; x++) {
+	    output->data.U8[y][x] = 0;
+	}
+    }
+
+    psTrace("stac.area", 3, "Finding area of interest....\n");
+
+
+    // Coordinates for the transformations
+    psPlane *inFrame = psAlloc(sizeof(psPlane));
+    psPlane *outFrame = psAlloc(sizeof(psPlane));
+    psPlane *deriv = psAlloc(sizeof(psPlane));
+
+    // Find pixels on the mask that are of interest
+    for (int y = 0; y < nyIn; y++) {
+	for (int x = 0; x < nxIn; x++) {
+	    if (mask->data.U8[y][x]) {
+		inFrame->x = (double)x + 0.5;
+		inFrame->y = (double)y + 0.5;
+		(void)psPlaneTransformApply(outFrame, map, inFrame);
+		(void)stacPlaneTransformDeriv(deriv, map, inFrame);
+
+		// Calculate range
+		int iMin = MAX((int)(outFrame->x - 0.5*deriv->x), 0);
+		int iMax = MIN((int)(outFrame->x + 0.5*deriv->x + 0.5), nxOut - 1);
+		int jMin = MAX((int)(outFrame->y - 0.5*deriv->y), 0);
+		int jMax = MIN((int)(outFrame->y + 0.5*deriv->y + 0.5), nyOut - 1);
+
+		for (int j = jMin; j <= jMax; j++) {
+		    for (int i = iMin; i <= iMax; i++) {
+			output->data.U8[j][i] = 1;
+		    }
+		}
+	    } // Pixel of interest
+	}
+    } // Iterating over input mask
+
+    psFree(inFrame);
+    psFree(outFrame);
+    psFree(deriv);
+
+    return output;
+}
Index: /tags/rel-0_0_1/stac/src/stacCheckMemory.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacCheckMemory.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacCheckMemory.c	(revision 7462)
@@ -0,0 +1,63 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "stac.h"
+
+
+#define LEAKS "leaks.dat"               // File to which to write leaks data
+
+
+psMemId stacMemPrint(const psMemBlock *ptr)
+{
+    psLogMsg("stac.memoryPrint", PS_LOG_INFO,
+             "Memory block %d:\n"
+             "\tFile %s, line %d, size %d\n"
+             "\tPosts: %x %x %x\n",
+             ptr->id, ptr->file, ptr->lineno, ptr->userMemorySize, ptr->startblock, ptr->endblock,
+             *(void**)((int8_t *)(ptr + 1) + ptr->userMemorySize));
+    return 0;
+}
+
+
+void stacMemoryProblem(const psMemBlock* ptr, ///< the pointer to the problematic memory block.
+                       const char *file, ///< the file in which the problem originated
+                       psS32 lineno     ///< the line number in which the problem originated
+    )
+{
+    psLogMsg("stac.checkMemory.corruption", PS_LOG_WARN,
+             "Memory corruption detected in memBlock %d\n"
+             "\tFile %s, line %d, size %d\n"
+             "\tPosts: %x %x %x\n",
+             ptr->id, file, lineno, ptr->userMemorySize, ptr->startblock, ptr->endblock,
+             (ptr + 1 + ptr->userMemorySize));
+}
+
+
+
+void stacCheckMemory(void)
+{
+    psMemBlock **leaks = NULL;          // List of leaks
+    FILE *leakFile;                     // File to write leaks to
+
+    psTrace("stac.checkMemory", 1, "Checking for memory problems....\n");
+
+    (void)psMemProblemCallbackSet((psMemProblemCallback)stacMemoryProblem); // Set callback for corruption
+
+    if ((leakFile = fopen(LEAKS, "w")) == NULL) {
+        fprintf(stderr, "Unable to open leaks file, %s\n", LEAKS);
+        return;
+    }
+
+    int nLeaks = psMemCheckLeaks(0, &leaks, leakFile, false); // Number of leaks
+    psTrace("stac.checkMemory", 1, "%d leaks found.\n", nLeaks);
+    for (int i = 0; i < nLeaks; i++) {
+        psLogMsg("stac.checkMemory.leaks", PS_LOG_WARN,
+                 "Memory leak detection: memBlock %d\n"
+                 "\tFile %s, line %d, size %d\n",
+                 leaks[i]->id, leaks[i]->file, leaks[i]->lineno, leaks[i]->userMemorySize);
+    }
+
+    int nCorrupted;                     // Number of corrupted memory blocks
+    nCorrupted = psMemCheckCorruption(false);
+    psTrace("stac.checkMemory", 1, "%d memory blocks corrupted.\n", nCorrupted);
+
+}
Index: /tags/rel-0_0_1/stac/src/stacCombine.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacCombine.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacCombine.c	(revision 7462)
@@ -0,0 +1,219 @@
+#include <stdio.h>
+#include <assert.h>
+#include <math.h>
+#include "pslib.h"
+#include "stac.h"
+
+#define SQUARE(x) ((x)*(x))
+#define ABS(x) ((x) >= 0 ? (x) : -(x))
+
+float stacCombineMean(psVector *values, // Values for which to take the mean
+                      psVector *errors, // Errors in the values
+                      psVector *masks   // Masks for the values, 1 = don't use, 0 = use
+    )
+{
+#if 0
+    // Would like to use psVectorStats, but it doesn't have errors built in yet
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN);
+    (void)psVectorStats(stats, values, NULL, masks, 1);
+    float mean = stats->sampleMean;
+    psFree(stats);
+    return mean;
+#else
+    // Instead, do it ourselves
+    double sum = 0.0;
+    double weights = 0.0;
+    int num = values->n;
+    for (int i = 0; i < num; i++) {
+        if (! masks->data.U8[i]) {
+            // "error" here is the variance
+            sum += values->data.F32[i] / errors->data.F32[i];
+            weights += 1.0 / errors->data.F32[i];
+        }
+    }
+    if (weights > 0.0) {
+        return (float)(sum/weights);
+    } else {
+        return NAN;
+    }
+#endif
+
+}
+
+
+float stacCombineMedian(psVector *values, // Values for which to take the median
+                        psVector *errors, // Errors in the values
+                        psVector *masks // Masks for the values, 0 = don't use, 1 = use
+    )
+{
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    (void)psVectorStats(stats, values, NULL, masks, 1);
+    float median = stats->sampleMedian;
+    psFree(stats);
+    return median;
+}
+
+
+
+bool stacCombine(psImage **combined,    // The combined image for output
+                 psArray **rejected,    // Array of rejection masks
+                 psArray *images,       // Array of transformed images
+                 psArray *errors,       // Array of transformed error images
+                 int nReject,           // Number of rejection iterations
+                 psImage *region,       // Region to combine
+                 psVector *saturated,   // Saturation limits for each image
+                 psVector *bad,         // Bad pixel limits for each image
+                 float reject           // Rejection (k-sigma)
+    )
+{
+    assert(combined);
+    assert(images);
+    assert(errors);
+    assert(images->n == errors->n);
+    assert(saturated);
+    assert(bad);
+    assert(saturated->n == images->n);
+    assert(bad->n == images->n);
+
+    int nImages = images->n;            // Number of images
+    int numRows = ((psImage*)images->data[0])->numRows; // Image size
+    int numCols = ((psImage*)images->data[0])->numCols; // Image size
+
+    // Check dimensions for consistency
+    for (int i = 0; i < nImages; i++) {
+        psImage *image = (psImage *)images->data[i]; // The image
+        psImage *error = (psImage *)errors->data[i]; // The error image
+
+        assert(image->numCols == numCols && image->numRows == numRows);
+        assert(error->numCols == numCols && error->numRows == numRows);
+    }
+
+    // Check combined image
+    assert(!*combined || ((*combined)->numRows == numRows && (*combined)->numCols == numCols));
+    if (*combined == NULL) {
+        *combined = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Combined image
+    }
+
+    // Check area of interest
+    assert(!region || (region->numRows == numRows && region->numCols == numCols));
+
+    psTrace("stac.combine", 1, "Combining images....\n");
+
+    psVector *pixels = psVectorAlloc(nImages, PS_TYPE_F32); // Will hold the pixels in the statistics step
+    psVector *deltas = psVectorAlloc(nImages, PS_TYPE_F32); // Will hold the errors in the statistics step
+    psVector *mask = psVectorAlloc(nImages, PS_TYPE_U8); // Will hold the mask in the statistics step
+
+    // Set up rejection masks
+    if (nReject > 0) {
+        if (*rejected == NULL) {
+            // Allocate the rejection masks, if required
+            *rejected = psArrayAlloc(nImages);
+        } else {
+            assert((*rejected)->n != nImages);
+        }
+
+        // Create and initialise rejection masks
+        for (int i = 0; i < nImages; i++) {
+            (*rejected)->data[i] = psImageAlloc(numCols, numRows, PS_TYPE_U8);
+            for (int r = 0; r < numRows; r++) {
+                for (int c = 0; c < numCols; c++) {
+                    ((psImage*)((*rejected)->data[i]))->data.U8[r][c] = 0;
+                }
+            }
+        }
+    }
+
+#ifdef TESTING
+    // chi^2 image
+    psImage *chi2 = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+    static int iteration = 0;           // Number of times function has been called
+#endif
+
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+
+            // Only combine those pixels requested
+            if (!region || (region && region->data.U8[y][x])) {
+
+                // Export pixels into the vector and get stats
+                for (int i = 0; i < nImages; i++) {
+                    float pixel = ((psImage*)images->data[i])->data.F32[y][x];
+                    float delta = ((psImage*)errors->data[i])->data.F32[y][x]; // This is the variance!
+                    pixels->data.F32[i] = pixel;
+                    deltas->data.F32[i] = delta;
+                    if ((pixel >= saturated->data.F32[i]) || (pixel <= bad->data.F32[i]) ||
+                        (! isfinite(pixel)) || (! isfinite(delta))) {
+                        mask->data.U8[i] = (psU8)1; // Don't use!
+                    } else {
+                        mask->data.U8[i] = (psU8)0; // Use.
+                    }
+                }
+
+                float average = stacCombineMean(pixels, deltas, mask); // Combined value
+
+                // We set the value BEFORE the rejection iteration because not all pixels that we reject
+                // here will be rejected in the final cut.
+                (*combined)->data.F32[y][x] = average;
+
+#ifdef TESTING
+                // Calculate chi^2
+                chi2->data.F32[y][x] = 0.0;
+                int numGoodPix = 0;
+                for (int i = 0; i < nImages; i++) {
+                    if (! mask->data.U8[i]) {
+                        chi2->data.F32[y][x] += SQUARE(pixels->data.F32[i] - average) / deltas->data.F32[i];
+                        numGoodPix++;
+                    }
+                }
+                chi2->data.F32[y][x] /= (float)numGoodPix;
+#endif
+
+                // Rejection iterations
+                bool keepGoing = true;  // Keep going with rejection?
+                for (int rejNum = 0; (rejNum < nReject) && keepGoing; rejNum++) {
+                    float max = 0.0;    // Maximum deviation
+                    int maxIndex = -1;  // Index of the maximum deviation
+                    for (int i = 0; i < nImages; i++) {
+                        if (!mask->data.U8[i] &&
+                            ((pixels->data.F32[i] - average) / sqrtf(deltas->data.F32[i]) > max)) {
+                            max = (pixels->data.F32[i] - average) / sqrtf(deltas->data.F32[i]);
+                            maxIndex = i;
+                        }
+                    }
+                    // Reject the pixel with the maximum deviation
+                    if (max > reject) {
+                        mask->data.U8[maxIndex] = 1;
+                        ((psImage*)((*rejected)->data[maxIndex]))->data.U8[y][x] += 1;
+                        // Re-do combination following rejection
+                        average = stacCombineMean(pixels, deltas, mask);
+                    } else {
+                        keepGoing = false;
+                    }
+                }
+
+            } // Pixels of interest
+
+        }
+    } // Iterating over output pixels
+
+#ifdef TESTING
+    // Write chi^2 image
+    iteration++;
+    char chiName[MAXCHAR];              // Filename of chi^2 image
+    sprintf(chiName,"chi2_%d.fits",iteration);
+    psFits *chiFile = psFitsAlloc(chiName);
+    if (!psFitsWriteImage(chiFile, NULL, chi2 , 0)) {
+        psErrorStackPrint(stderr, "Unable to write image: %s\n", chiName);
+    }
+    psTrace("stac.combine", 1, "Chi^2 image written to %s\n", chiName);
+    psFree(chiFile);
+    psFree(chi2);
+#endif
+
+    // Clean up
+    psFree(pixels);
+    psFree(deltas);
+    psFree(mask);
+
+    return combined;
+}
Index: /tags/rel-0_0_1/stac/src/stacConfig.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacConfig.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacConfig.c	(revision 7462)
@@ -0,0 +1,176 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <getopt.h>
+#include <string.h>
+#include "pslib.h"
+#include "stac.h"
+#include "stacConfig.h"
+
+stacConfig *stacConfigAlloc(void)
+{
+    // Allocate memory
+    stacConfig *config = (stacConfig*) psAlloc(sizeof(stacConfig));
+    // Set defaults
+    config->verbose = 0;
+    config->gain = 1.0;
+    config->readnoise = 0.0;
+    config->inputs = NULL;
+    config->output = NULL;
+    config->saveShifts = false;
+    config->starFile = NULL;
+    config->starMapFile = NULL;
+    config->aper = 3.0;
+    config->outnx = 0;
+    config->outny = 0;
+    config->saturated = 65535.0;	// Saturation level
+    config->bad = 0.0;			// Bad level
+    config->reject = 2.5;
+    config->frac = 0.45;
+    config->grad = 0.7;
+    config->nReject = 2;
+
+    return config;
+}
+
+
+void stacConfigFree(stacConfig *config)
+{
+    // Free the vectors, if necessary
+    if (config->inputs) {
+	psFree(config->inputs);
+    }
+    // Free everything
+    psFree(config);
+}
+
+
+stacConfig *stacParseConfig(int argc,	// Number of command-line arguments
+			    char **argv // Command-line arguments
+    )
+{
+    stacConfig *config = stacConfigAlloc(); // Configuration values
+    const char *programName = argv[0];	// Program name
+
+    /* Variables for getopt */
+    int opt;   /* Option, from getopt */
+    extern char *optarg;   /* Argument accompanying switch */
+    extern int optind;   /* getopt variables */
+
+    /* Parse command-line arguments using getopt */
+    while ((opt = getopt(argc, argv, "hvSg:r:o:s:b:k:n:f:G:p:a:")) != -1) {
+        switch (opt) {
+	  case 'h':
+	    help(programName);
+	    exit(EXIT_SUCCESS);
+	  case 'v':
+            config->verbose++;
+            break;
+	  case 'g':
+	    if (sscanf(optarg, "%f", &config->gain) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'r':
+	    if (sscanf(optarg, "%f", &config->readnoise) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'o':
+            if ((sscanf(argv[optind-1], "%d", &config->outnx) != 1) ||
+                (sscanf(argv[optind++], "%d", &config->outny) != 1)) {
+                // Note: incrementing optind, so I can read more than one parameter.
+                help(programName);
+                exit(EXIT_FAILURE);
+            }
+	    break;
+	  case 's':
+	    if (sscanf(optarg, "%f", &config->saturated) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'b':
+	    if (sscanf(optarg, "%f", &config->bad) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'p':
+	    if (argc < optind+1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    config->starFile = argv[optind-1];
+	    config->starMapFile = argv[optind++];
+	    // Note: incrementing optind, so I can read more than one parameter.
+	    break;
+	  case 'a':
+	    if (sscanf(optarg, "%f", &config->aper) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'k':
+	    if (sscanf(optarg, "%f", &config->reject) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'n':
+	    if (sscanf(optarg, "%d", &config->nReject) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'f':
+	    if (sscanf(optarg, "%f", &config->frac) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'G':
+	    if (sscanf(optarg, "%f", &config->grad) != 1) {
+		help(programName);
+		exit(EXIT_FAILURE);
+	    }
+	    break;
+	  case 'S':
+	    config->saveShifts = true;
+	    break;
+	  default:
+	    help(programName);
+	}
+    }
+
+    /* Get the remaining, mandatory values */
+    argc -= optind;
+    argv += optind;
+    if (argc < 2) {
+        help(programName);
+        exit(EXIT_FAILURE);
+    }
+
+    config->output = argv[0];		// Output file
+    // Get the input files
+    config->inputs = psArrayAlloc(argc-1);
+    for (int i = 1; i < argc; i++) {
+	config->inputs->data[i-1] = psAlloc(strlen(argv[i]));
+	strncpy(config->inputs->data[i-1], argv[i], strlen(argv[i]));
+    }
+
+    // Debugging output
+    psTrace("stac.config", 8, "Parsed command line for configuration\n");
+    psTrace("stac.config", 9, "Verbosity level: %d\n",config->verbose);
+    psTrace("stac.config", 9, "%d inputs:\n",config->inputs->n);
+    for (int i = 0; i < config->inputs->n; i++) {
+	psTrace("stac.config", 9, "\t%s\n", config->inputs->data[i]);
+    }
+    psTrace("stac.config", 9, "Output file is %s\n",config->output);
+    psTrace("stac.config", 9, "Output file size is %dx%d\n", config->outnx, config->outny);
+
+    return config;
+}
+
Index: /tags/rel-0_0_1/stac/src/stacConfig.h
===================================================================
--- /tags/rel-0_0_1/stac/src/stacConfig.h	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacConfig.h	(revision 7462)
@@ -0,0 +1,43 @@
+#ifndef STAC_CONFIG_H
+#define STAC_CONFIG_H
+
+#include "pslib.h"
+
+// stacConfig.c
+
+// Configuration options
+typedef struct {
+    int verbose;			// Verbosity level
+    float gain, readnoise;		// Gain and readnoise for detectors
+    psArray *inputs;			// Input file names
+    const char *output;			// Output file name
+    bool saveShifts;			// Save shifted images?
+    const char *starFile;		// File with star coordinates
+    const char *starMapFile;		// File with map for stars
+    int outnx, outny;			// Size of output image
+    float xOffset, yOffset;		// Offset to get lower-left corner at 0,0
+    float saturated;			// Saturation level
+    float bad;				// Bad level
+    float reject;			// Rejection level
+    float frac;				// Fraction of input pixel that must be masked before the pixel is
+					// considered bad
+    float grad;				// Multiplier of the gradient
+    int nReject;			// Number of rejection iterations
+    float xMapDiff, yMapDiff;		// Difference between pure map and output image coordinates
+    float aper;				// Aperture size (pixels)
+} stacConfig;
+
+// Allocator
+stacConfig *stacConfigAlloc(void);
+// Deallocator
+void stacConfigFree(stacConfig *config);
+
+// Help message
+void help(const char *name);
+
+// Parse the command line and return config
+stacConfig *stacParseConfig(int argc,	// Number of command-line arguments
+			    char **argv // Command-line arguments
+    );
+
+#endif
Index: /tags/rel-0_0_1/stac/src/stacErrorImages.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacErrorImages.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacErrorImages.c	(revision 7462)
@@ -0,0 +1,38 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "stac.h"
+
+psArray *stacErrorImages(psArray *inputs, // Array of input images
+			 float gain,	// Gain, in e/ADU
+			 float rn	// Read noise, in e
+    )
+{
+    float invGain = 1.0/gain;		// Inverse square root of gain
+    rn /= gain;				// Read noise in ADU
+    psArray *errors = psArrayAlloc(inputs->n);
+
+    psTrace("stac.errors", 1, "Calculating error images....\n");
+
+    // Iterate over the input images
+    for (int i = 0; i < inputs->n; i++) {
+	psTrace("stac.errors",5,"Working on image #%d\n",i);
+
+	psImage *image = inputs->data[i]; // Pull out the image of interest
+	int numRows = image->numRows;	// Number of rows
+	int numCols = image->numCols;	// Number of columns	
+	psImage *error = psImageAlloc(numCols, numRows, PS_TYPE_F32); // The error image
+
+	// Iterate over the pixels
+	for (int r = 0; r < numRows; r++) {
+	    for (int c = 0; c < numCols; c++) {
+		// We actually calculate the variance
+		error->data.F32[r][c] = image->data.F32[r][c]*invGain + rn*rn;
+	    }
+	}
+
+	// Put image onto the array
+	errors->data[i] = error;
+    }
+
+    return errors;
+}
Index: /tags/rel-0_0_1/stac/src/stacHelp.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacHelp.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacHelp.c	(revision 7462)
@@ -0,0 +1,27 @@
+#include <stdio.h>
+
+void help(const char *name)
+{
+    fprintf (stderr, "STAC: Simultaneous Telescope Array Combination\n"
+	     "Usage: %s [-h] [-v] [-g GAIN] [-r RN] [-o NX NY] [-S] [-s SAT] [-b BAD] [-p FILE MAP] [-a APER] [-k REJ] [-n NREJECT] [-k FRAC] [-G GRAD] OUT IN1 IN2...\n"
+	     "where\n"
+	     "\t-h           Help (this info)\n"
+	     "\t-v           Increase verbosity level\n"
+	     "\t-g           Gain (e/ADU) for detectors\n"
+	     "\t-r           Read noise (e) for detectors\n"
+	     "\t-o NX NY     Size of output image (512, 512)\n"
+	     "\t-S           Save shifted images (false)\n"
+	     "\t-s SAT       Saturation level (65535)\n"
+	     "\t-b BAD       Bad level (0)\n"
+	     "\t-p FILE MAP  Specify file containing star coordinates, with map\n"
+	     "\t-a APER      Aperture size to use for photometry (3 pix)\n"
+	     "\t-k REJ       Rejection level (k-sigma; 3.5)\n"
+	     "\t-n NREJECT   Number of rejection iterations (2)\n"
+	     "\t-f FRAC      Fraction of pixel to be marked before considered bad (0.5)\n"
+	     "\t-d GRAD      Gradient threshold for pixel to be marked (0.0)\n"
+	     "\tOUT          Output image\n"
+	     "\tIN1, IN2...  Input images, which have associated .map files.\n",
+	     name
+	);
+}
+
Index: /tags/rel-0_0_1/stac/src/stacInvertMaps.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacInvertMaps.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacInvertMaps.c	(revision 7462)
@@ -0,0 +1,203 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+#define NUM_GRID 20
+
+psArray *stacInvertMaps(const psArray *maps, // Array of maps to invert
+                        int outnx, int outny // Size of output image
+    )
+{
+    int nMaps = maps->n;                // Number of maps
+    psArray *inverted = psArrayAlloc(nMaps); // Array of inverted maps for output
+
+    psTrace("stac.invertMaps", 1, "Inverting maps....\n");
+
+    // Coordinates for the transformations
+    psPlane *inCoord = psAlloc(sizeof(psPlane));
+    psPlane *outCoord = psAlloc(sizeof(psPlane));
+
+    for (int mapNum = 0; mapNum < nMaps; mapNum++) {
+
+        psPlaneTransform *oldMap = (psPlaneTransform*)maps->data[mapNum]; // Uninverted map
+        // Check input
+        assert(oldMap->x->nX == oldMap->x->nY && oldMap->y->nX == oldMap->y->nY &&
+               oldMap->x->nX == oldMap->y->nX);
+        int order = oldMap->x->nX;      // Polynomial order
+        psTrace("stac.invertMaps", 4, "Generating order %d polynomial inverse transformation.\n", order);
+        psPlaneTransform *newMap = psPlaneTransformAlloc(order, order); // Inverted map
+
+        // Create fake polynomial to use in evaluation
+        psPolynomial2D *fakePoly = psPolynomial2DAlloc(order, order, PS_POLYNOMIAL_ORD);
+        for (int i = 0; i <= order; i++) {
+            for (int j = 0; j <= order; j++) {
+                fakePoly->coeff[i][j] = 1.0; // Set all coeffecients to 1
+                fakePoly->mask[i][j] = 1; // Mask all coefficients; unmask to evaluate
+            }
+        }
+
+        // A grid of xin,yin --> xout,yout
+        psVector *xIn = psVectorAlloc(NUM_GRID * NUM_GRID, PS_TYPE_F32);
+        psVector *yIn = psVectorAlloc(NUM_GRID * NUM_GRID, PS_TYPE_F32);
+        psVector *xOut = psVectorAlloc(NUM_GRID * NUM_GRID, PS_TYPE_F32);
+        psVector *yOut = psVectorAlloc(NUM_GRID * NUM_GRID, PS_TYPE_F32);
+
+        // Create grid of points
+        for (int yint = 0; yint < NUM_GRID; yint++) {
+            inCoord->y = (float)(yint * outny) / (float)(NUM_GRID - 1);
+            for (int xint = 0; xint < NUM_GRID; xint++) {
+                inCoord->x = (float)(xint * outnx) / (float)(NUM_GRID - 1);
+
+                (void)psPlaneTransformApply(outCoord, oldMap, inCoord);
+
+                xOut->data.F32[yint*NUM_GRID + xint] = inCoord->x;
+                yOut->data.F32[yint*NUM_GRID + xint] = inCoord->y;
+                xIn->data.F32[yint*NUM_GRID + xint] = outCoord->x;
+                yIn->data.F32[yint*NUM_GRID + xint] = outCoord->y;
+            }
+        }
+
+        // Initialise the matrix and vectors
+        int nCoeff = (order + 1) * (order + 2) / 2; // Number of polynomial coefficients
+        psImage *matrix = psImageAlloc(nCoeff, nCoeff, PS_TYPE_F64); // Matrix for solution
+        psVector *xVector = psVectorAlloc(nCoeff, PS_TYPE_F64); // Vector for solution in x
+        psVector *yVector = psVectorAlloc(nCoeff, PS_TYPE_F64); // Vector for solution in y
+        for (int i = 0; i < nCoeff; i++) {
+            for (int j = 0; j < nCoeff; j++) {
+                matrix->data.F64[i][j] = 0.0;
+            }
+            xVector->data.F64[i] = 0.0;
+            yVector->data.F64[i] = 0.0;
+        }
+
+        // Iterate over the grid points
+        for (int g = 0; g < NUM_GRID*NUM_GRID; g++) {
+
+            // Iterate over the polynomial coefficients, accumulating the matrix and vectors
+            for (int i = 0, ijIndex = 0; i <= order; i++) {
+                for (int j = 0; j <= order - i; j++, ijIndex++) {
+
+                    fakePoly->mask[i][j] = 0;
+                    double ijPoly = psPolynomial2DEval(fakePoly, xIn->data.F32[g], yIn->data.F32[g]);
+                    fakePoly->mask[i][j] = 1;
+
+                    for (int m = 0, mnIndex = 0; m <= order; m++) {
+                        for (int n = 0; n <= order - m; n++, mnIndex++) {
+
+                            fakePoly->mask[m][n] = 0;
+                            double mnPoly = psPolynomial2DEval(fakePoly, xIn->data.F32[g], yIn->data.F32[g]);
+                            fakePoly->mask[m][n] = 1;
+
+                            matrix->data.F64[ijIndex][mnIndex] += ijPoly * mnPoly;
+                        }
+                    }
+
+                    xVector->data.F64[ijIndex] += ijPoly * (double)xOut->data.F32[g];
+                    yVector->data.F64[ijIndex] += ijPoly * (double)yOut->data.F32[g];
+                }
+            } // Iterating over coefficients
+        } // Iterating over grid points
+
+        // Solution via LU Decomposition
+        psVector *permutation = psVectorAlloc(nCoeff, PS_TYPE_F64); // Permutation vector for LU Decomposition
+        psImage *luMatrix = psMatrixLUD(NULL, &permutation, matrix); // LU decomposed matrix
+        psVector *xSolution = psMatrixLUSolve(NULL, luMatrix, xVector, permutation); // Solution in x
+        psVector *ySolution = psMatrixLUSolve(NULL, luMatrix, yVector, permutation); // Solution in y
+
+        // Stuff coefficients into transformation
+        for (int i = 0, ijIndex = 0; i <= order; i++) {
+            for (int j = 0; j <= order - i; j++, ijIndex++) {
+                newMap->x->coeff[i][j] = xSolution->data.F64[ijIndex];
+                newMap->y->coeff[i][j] = ySolution->data.F64[ijIndex];
+            }
+        }
+        inverted->data[mapNum] = newMap;
+
+#ifdef TESTING
+        // Print x coefficients
+        psTrace("stac.invertMaps", 7, "x' = \n");
+        for (int i = 0; i <= order; i++) {
+            for (int j = 0; j <= order - i; j++) {
+                psTrace("stac.invertMaps", 7, "      %f x^%d y^%d\n", newMap->x->coeff[i][j], i, j);
+            }
+        }
+        // Print y coefficients
+        psTrace("stac.invertMaps", 7, "y' = \n");
+        for (int i = 0; i <= order; i++) {
+            for (int j = 0; j <= order - i; j++) {
+                psTrace("stac.invertMaps", 7, "      %f x^%d y^%d\n", newMap->y->coeff[i][j], i, j);
+            }
+        }
+#endif
+
+#ifdef TESTING
+        // Go forward then backward
+        double x = 123.4;
+        double y = 432.1;
+        psPlane *oldCoords = psAlloc(sizeof(psPlane));
+        oldCoords->x = x;
+        oldCoords->y = y;
+        psPlane *newCoords = psPlaneTransformApply(NULL, oldMap, oldCoords);
+        psTrace("stac.invertMaps.test", 5, "%f,%f --> %f,%f\n", x, y, newCoords->x, newCoords->y);
+        (void)psPlaneTransformApply(oldCoords, newMap, newCoords);
+        psTrace("stac.invertMaps.test", 5, "--------> %f,%f\n", oldCoords->x, oldCoords->y);
+        psFree(newCoords);
+        psFree(oldCoords);
+#endif
+
+        psFree(permutation);
+        psFree(luMatrix);
+        psFree(matrix);
+        psFree(xVector);
+        psFree(yVector);
+        psFree(xSolution);
+        psFree(ySolution);
+        psFree(fakePoly);
+        psFree(xIn);
+        psFree(yIn);
+        psFree(xOut);
+        psFree(yOut);
+    }
+
+    psFree(inCoord);
+    psFree(outCoord);
+
+
+#if 0
+        // Can't handle higher order than linear yet
+        assert(((psPlaneTransform*)maps->data[i])->x->nX == 2 &&
+               ((psPlaneTransform*)maps->data[i])->x->nY == 2 &&
+               ((psPlaneTransform*)maps->data[i])->y->nX == 2 &&
+               ((psPlaneTransform*)maps->data[i])->y->nY == 2);
+        psPlaneTransform *newMap = psPlaneTransformAlloc(2, 2); // Inverted map
+        psPlaneTransform *oldMap = (psPlaneTransform*)maps->data[i]; // Uninverted map
+
+        // Now, simply do a 2x2 matrix inversion
+
+        double a = oldMap->x->coeff[1][0];
+        double b = oldMap->x->coeff[0][1];
+        double c = oldMap->y->coeff[1][0];
+        double d = oldMap->y->coeff[0][1];
+        double e = oldMap->x->coeff[0][0];
+        double f = oldMap->y->coeff[0][0];
+
+        double invDet = 1.0 / (a * d - b * c); // Inverse of the determinant
+
+        // Not entirely sure why this works, but it appears to do so.......................................
+        newMap->x->coeff[1][0] = invDet * a;
+        newMap->x->coeff[0][1] = - invDet * b;
+        newMap->y->coeff[1][0] = - invDet * c;
+        newMap->y->coeff[0][1] = invDet * d;
+
+        newMap->x->coeff[0][0] = - invDet * (d * e + c * f);
+        newMap->y->coeff[0][0] = - invDet * (b * e + a * f);
+#endif
+
+
+    return inverted;
+}
+
+
Index: /tags/rel-0_0_1/stac/src/stacRead.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacRead.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacRead.c	(revision 7462)
@@ -0,0 +1,200 @@
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+#define BUFFER 100                      // Size of buffer for incrementally reading coordinates
+
+psArray *stacReadImages(psArray **headers, // The image headers, to be returned
+    psArray *filenames // The file names of the images
+    )
+{
+    int nFiles = filenames->n;          // The number of input files
+    psArray *images = psArrayAlloc(nFiles); // The input files, to be returned
+    assert(!headers || ! *headers || (*headers)->n == nFiles);
+    if (headers && ! *headers) {
+        *headers = psArrayAlloc(nFiles);
+    }
+
+    psRegion imageRegion = {0, 0, 0, 0}; // Region of image to read
+
+    psTrace("stac.read.images", 1, "Reading input images....\n");
+    for (int i = 0; i < nFiles; i++) {
+        psTrace("stac.read.images", 2, "Reading input image %s....\n",filenames->data[i]);
+        psFits *imageFile = psFitsOpen(filenames->data[i], "r");
+        // We only read PHUs --- not mucking around with extensions for now
+        if (headers) {
+            (*headers)->data[i] = psFitsReadHeader(NULL, imageFile);
+        }
+        psImage *image = psFitsReadImage(NULL, imageFile, imageRegion, 0);
+        if (image == NULL) {
+            psErrorStackPrint(stderr,"Fatal error: Unable to read %s\n",filenames->data[i]);
+            exit(EXIT_FAILURE);
+        }
+        psFitsClose(imageFile);
+        psTrace("stac.read.images", 4, "Image %s is %dx%d\n", filenames->data[i], image->numCols,
+                image->numRows);
+        // Convert to 32-bit floating point, in necessary
+        if (image->type.type != PS_TYPE_F32) {
+            psTrace("stac.read.images", 3, "Converting %s to floating point in memory....\n",
+                    filenames->data[i]);
+            images->data[i] = psImageCopy(NULL, image, PS_TYPE_F32);
+            psFree(image);
+        }
+        int numNaN = psImageClipNaN(image, 0.0);
+        if (numNaN) {
+            psTrace("stac.read.images", 5, "Clipped %d NaN pixels to zero.\n", numNaN);
+        }
+        images->data[i] = image;
+    }
+    psTrace("stac.read.images",1,"%d input images read.\n",nFiles);
+
+    return images;
+}
+
+psArray *stacReadCoords(const char *filename)
+{
+    FILE *file = fopen(filename, "r");
+    if (file == NULL) {
+        psLogMsg("stac.read.coords", PS_LOG_ERROR, "Cannot open coordinate file, %s\n", filename);
+        return NULL;
+    }
+
+    psTrace("stac.read.coords", 5, "Reading coordinate file, %s\n", filename);
+
+    psArray *coords = psArrayAlloc(BUFFER); // The array of coordinates to be returned
+    float x, y;                         // Coordinates to read
+    int num = 0;                        // Number of coordinates read
+    while (fscanf(file, "%f %f\n", &x, &y) == 2) {
+        psPlane *coord = psPlaneAlloc();// A coordinate
+        coord->x = x;
+        coord->y = y;
+        coords->data[num] = coord;
+        num++;
+        if (num % BUFFER) {
+            coords = psArrayRealloc(coords, num + BUFFER);
+        }
+    }
+    coords->n = num;
+
+    psTrace("stac.read.coords", 5, "%d coordinates read.\n", num);
+
+    fclose(file);
+    return coords;
+}
+
+
+psPlaneTransform *stacReadMap(const char *filename)
+{
+    FILE *mapfp = fopen(filename, "r");
+    if (mapfp == NULL) {
+        psLogMsg("stac.read.map", PS_LOG_ERROR, "Cannot open map file, %s\n", filename);
+        return NULL;
+    }
+    // Read the file
+    psTrace("stac.read.map", 5, "Reading map file %s....\n", filename);
+
+    // Format is now:
+    // order
+    // x coefficients
+    // y coefficients
+    //
+    // where the order is 1 for linear, 2 for quadratic, 3 for cubic.
+    // and the coefficients are read by the following pseudo-code:
+    //
+    //  for (int k = 0; k < order + 1; k++)
+    //      for (int j = 0; j < k; j++)
+    //          int i = k - j;
+    //              read coefficient of x^i y^j
+    //
+    // This produces the ordering:
+    // x^0 y^0, x^1 y^0, x^0 y^1, x^2 y^0, x^1 y^1, x^0 y^2, x^3 y^0, x^2 y^1, x^1 y^2, x^0 y^3
+    //
+    // This is, of course, for third order polynomials.
+    // For lower orders, the list is truncated at the appropriate level.
+
+    int order = 0;                      // Polynomial order
+    if (fscanf(mapfp, "%d", &order) != 1) {
+        psLogMsg("stac.read.map", PS_LOG_ERROR, "Unable to read map file %s\n", filename);
+        fclose(mapfp);
+        return NULL;
+    }
+
+    psTrace("stac.read.map", 5, "Polynomial order: %d\n", order);
+
+    psPlaneTransform *map = psPlaneTransformAlloc(order, order); // The transformation
+    // Set coefficient masks
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order + 1) {
+                map->x->mask[i][j] = 1;
+                map->y->mask[i][j] = 1;
+            } else {
+                map->x->mask[i][j] = 0;
+                map->y->mask[i][j] = 0;
+            }
+        }
+    }
+
+    // Read x coefficients
+    psTrace("stac.read.map", 7, "x' = \n");
+    for (int k = 0; k <= order; k++) {
+        for (int j = 0; j <= k; j++) {
+            int i = k - j;
+            if (fscanf(mapfp, "%lf", &map->x->coeff[i][j]) != 1) {
+                psLogMsg("stac.read.map", PS_LOG_ERROR, "Unable to read map file %s\n", filename);
+                fclose(mapfp);
+                psFree(map);
+                return NULL;
+            }
+            psTrace("stac.read.map", 7, "      %f x^%d y^%d\n", map->x->coeff[i][j], i, j);
+        }
+    }
+    // Read y coefficients
+    psTrace("stac.read.maps", 7, "y' = \n");
+    for (int k = 0; k <= order; k++) {
+        for (int j = 0; j <= k; j++) {
+            int i = k - j;
+            if (fscanf(mapfp, "%lf", &map->y->coeff[i][j]) != 1) {
+                psLogMsg("stac.read.map", PS_LOG_ERROR, "Unable to read map file %s\n", filename);
+                fclose(mapfp);
+                psFree(map);
+                return NULL;
+            }
+            psTrace("stac.read.map", 7, "      %f x^%d y^%d\n", map->y->coeff[i][j], i, j);
+        }
+    }
+
+    fclose(mapfp);
+
+    return map;
+}
+
+
+
+psArray *stacReadMaps(psArray *filenames // The file names of the images whose maps are to be read
+    )
+{
+    int nFiles = filenames->n;          // The number of input files
+    psArray *maps = psArrayAlloc(nFiles); // The maps, to be returned
+    char mapfile[MAXCHAR];              // Filename of map
+
+    psTrace("stac.read.maps",1,"Reading maps....\n");
+    for (int i = 0; i < nFiles; i++) {
+        if (strlen(filenames->data[i]) > MAXCHAR - 4) {
+            psLogMsg("stac.read.maps",PS_LOG_ERROR,"Filename %s is too long.\n",filenames->data[i]);
+            exit(EXIT_FAILURE);
+        }
+        // Read the file
+        sprintf(mapfile, "%s.map", (const char*)filenames->data[i]);
+        maps->data[i] = stacReadMap(mapfile);
+        if (maps->data[i] == NULL) {
+            psLogMsg("stac.read.maps", PS_LOG_ERROR, "Unable to read map: %s\n", mapfile);
+        }
+
+    }
+    psTrace("stac.read.maps",1,"%d maps read.\n",nFiles);
+
+    return maps;
+}
Index: /tags/rel-0_0_1/stac/src/stacRejection.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacRejection.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacRejection.c	(revision 7462)
@@ -0,0 +1,241 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+#define MIN(x,y) ((x) < (y) ? (x) : (y))
+
+float stacGradient(psImage *image,	// Input for which to measure the gradient
+		   int x, int y		// Coordinates at which to measure the gradient
+    )
+{
+    int num = 0;
+    psVector *pixels = psVectorAlloc(8, PS_TYPE_F32); // Array of pixels
+    psVector *mask = psVectorAlloc(8, PS_TYPE_U8); // Corresponding mask
+
+    // Get limits
+    int xMin = MAX(x - 1, 0);
+    int xMax = MIN(x + 1, image->numCols - 1);
+    int yMin = MAX(y - 1, 0);
+    int yMax = MIN(y + 1, image->numRows - 1);
+    for (int j = yMin; j <= yMax; j++) {
+	for (int i = xMin; i <= xMax; i++) {
+	    if ((i != x) && (j != y)) {
+		pixels->data.F32[num] = image->data.F32[j][i];
+		mask->data.U8[num] = 0;
+		num++;
+	    }
+	}
+    }
+    pixels->n = num;
+    mask->n = num;
+
+    // Get the median
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    (void)psVectorStats(stats, pixels, NULL, mask, 1);
+    float median = stats->sampleMedian;
+    psFree(stats);
+    psFree(pixels);
+    psFree(mask);
+    return median / image->data.F32[y][x];
+}
+
+psArray *stacRejection(psArray *inputs,	// Input images
+		       psArray *rejected, // Rejected images
+		       psArray *regions, // Regions of interest
+		       psArray *maps,	// Maps from input to transformed image
+		       psArray *inverseMaps, // Maps from transformed to input image
+		       float frac,	// Fraction of pixel rejected before looking more carefully
+		       float grad,	// Gradient limit for rejection
+		       psArray *names	// Names of original images (only for writing out when TESTING)
+    )
+{
+    int nImages = inputs->n;		// Number of input images
+
+    // Check inputs
+    assert(inputs->n == rejected->n);
+    assert(inputs->n == regions->n);
+    assert(inputs->n == maps->n);
+    assert(inputs->n == inverseMaps->n);
+    assert(!names || names->n == inputs->n);
+
+    for (int i = 0; i < nImages; i++) {
+	psImage *input = inputs->data[i];
+	psImage *region = regions->data[i];
+	assert(input->numRows == region->numRows && input->numCols == region->numCols);
+    }
+
+    // Stuff for the transformations
+    psPlane *inCoords = psAlloc(sizeof(psPlane)); // Coordinates on the input
+    psPlane *outCoords = psAlloc(sizeof(psPlane)); // Coordinates on the output
+
+    psTrace("stac.rejection", 1, "Mapping rejection masks back to source....\n");
+
+    // Vectors for calculating the mean gradient
+    psVector *grads = psVectorAlloc(nImages, PS_TYPE_F32); // Gradient for each image
+    psVector *gradsMask = psVectorAlloc(nImages, PS_TYPE_U8); // Mask for gradient vector
+
+    // Transform rejection masks back to source
+    psArray *inputRej = psArrayAlloc(nImages);
+    for (int i = 0; i < nImages; i++) {
+	// Size of input image
+	int nxInput = ((psImage*)(inputs->data[i]))->numCols;
+	int nyInput = ((psImage*)(inputs->data[i]))->numRows;
+	psImage *mask = psImageAlloc(nxInput, nyInput, PS_TYPE_U8); // The pixel mask for the input
+#ifdef TESTING
+	psImage *rejmap = psImageAlloc(nxInput, nyInput, PS_TYPE_F32); // The rejections in the source frame
+	psImage *gradient = psImageAlloc(nxInput, nyInput, PS_TYPE_F32);	// The gradient image
+#endif
+	psImage *reject = rejected->data[i]; // Pull out the mask in the output frame
+	psPlaneTransform *map = maps->data[i]; // The map from input to output
+	psImage *region = regions->data[i]; // The region of interest for this image
+
+	psTrace("stac.rejection", 3, "Transforming rejection mask %d....\n", i);
+	int nBad = 0;			// Number of bad pixels
+
+#ifdef CRFLUX
+	// Set up CR output
+	FILE *crs = NULL;		// File for outputting details of rejected pixels
+	char crfile[MAXCHAR];	// Filename
+	sprintf(crfile,"%s.cr",names->data[i]);
+	if ((crs = fopen(crfile, "w")) == NULL) {
+	    fprintf(stderr, "Unable to open file for detailed output, %s\n");
+	    return NULL;
+	}
+#endif
+
+	// Transform the mask
+	// Optimisation option is to only transform the pixels that have been rejected in the output,
+	// calculate derivatives of the map, and use that as a buffer around the transformed position
+	// in the input image.
+	psStats *median = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+	for (int y = 0; y < nyInput; y++) {
+	    for (int x = 0; x < nxInput; x++) {
+
+		// Only transform pixels of interest
+		if (region->data.U8[y][x]) {
+
+		    inCoords->x = (double)x + 0.5;
+		    inCoords->y = (double)y + 0.5;
+		    (void)psPlaneTransformApply(outCoords, map, inCoords);
+		    float maskVal = (float)psImagePixelInterpolate(reject, outCoords->x, outCoords->y,
+								   NULL, 0, 0.0, PS_INTERPOLATE_BILINEAR);
+#ifdef TESTING
+		    rejmap->data.F32[y][x] = maskVal;
+#endif
+
+		    if (maskVal > frac) {
+			// Calculate mean gradient on other images
+			float meanGrads = 0.0;
+			int numGrads = 0;
+			for (int j = 0; j < nImages; j++) {
+			    if (i != j) {
+				// Get coordinates for that image
+				(void)psPlaneTransformApply(inCoords, inverseMaps->data[j], outCoords);
+				int xPix = (int)(inCoords->x + 0.5);
+				int yPix = (int)(inCoords->y + 0.5);
+				if ((xPix >= 0) && (xPix <= ((psImage*)(inputs->data[j]))->numCols - 1) &&
+				    (yPix >= 0) && (yPix <= ((psImage*)(inputs->data[j]))->numRows - 1)) {
+				    // Calculate the gradient
+				    grads->data.F32[j] = stacGradient(inputs->data[j], xPix, yPix);
+				    gradsMask->data.U8[j] = 0;
+				    numGrads++;
+				} else {
+				    gradsMask->data.U8[j] = 1; // Mask this one
+				}
+			    } else {
+				gradsMask->data.U8[j] = 1; // Mask this one
+			    }
+			}
+			if (numGrads > 0) {
+			    (void)psVectorStats(median, grads, NULL, gradsMask, (psU8)1);
+			    meanGrads = median->sampleMedian;
+			} else {
+			    meanGrads = 0.0;
+			}
+
+#ifdef TESTING
+			//gradient->data.F32[y][x] = stacGradient(inputs->data[i], x, y) / meanGrads;
+			gradient->data.F32[y][x] = meanGrads;
+#endif
+
+			//if (stacGradient(inputs->data[i], x, y) < grad * meanGrads) {
+			if (meanGrads > grad) {
+			    mask->data.U8[y][x] = 1;
+			    nBad++;
+
+#ifdef CRFLUX
+			    fprintf(crs, "%d %d --> %f %f %f\n", x, y,
+				    ((psImage*)(inputs->data[i]))->data.F32[y][x], maskVal,
+				    stacGradient(inputs->data[i], x, y));
+#endif
+
+			} else {
+			    mask->data.U8[y][x] = 0;
+			} // Gradient threshold
+		    } else {
+			mask->data.U8[y][x] = 0;
+		    } // Rejection threshold
+
+		} else {
+		    mask->data.U8[y][x] = 0;
+		} // Only touching pixels of interest
+
+	    }
+	} // Iterating over pixels
+	psFree(median);
+
+#ifdef CRFLUX
+	// Close file used for detailed output
+	fclose(crs);
+#endif
+
+	psTrace("stac.rejection", 1, "%d pixels masked in image %d\n", nBad, i);
+	// Clip the image, and convert to suitable mask format
+
+#ifdef TESTING
+	// Write error image out to check
+	char maskName[MAXCHAR];		// Filename of mask image
+	char rejmapName[MAXCHAR]; 	// Filename of rejection image
+	char gradName[MAXCHAR];		// Filename of gradient image
+	sprintf(maskName, "%s.mask", names->data[i]);
+	sprintf(rejmapName, "%s.rejmap", names->data[i]);
+	sprintf(gradName, "%s.grad", names->data[i]);
+
+	psFits *maskFile = psFitsAlloc(maskName);
+	psFits *rejmapFile = psFitsAlloc(rejmapName);
+	psFits *gradFile = psFitsAlloc(gradName);
+	if (!psFitsWriteImage(maskFile, NULL, mask, 0)) {
+	    psErrorStackPrint(stderr, "Unable to write image: %s\n", maskName);
+	}
+	psTrace("stac", 1, "Mask image written to %s\n", maskName);
+	if (!psFitsWriteImage(rejmapFile, NULL, rejmap, 0)) {
+	    psErrorStackPrint(stderr, "Unable to write image: %s\n", rejmapName);
+	}
+	psTrace("stac", 1, "Rejection map written to %s\n", rejmapName);
+	if (!psFitsWriteImage(gradFile, NULL, gradient, 0)) {
+	    psErrorStackPrint(stderr, "Unable to write image: %s\n", gradName);
+	}
+	psTrace("stac", 1, "Gradient image written to %s\n", gradName);
+	psFree(maskFile);
+	psFree(rejmapFile);
+	psFree(gradFile);
+	psFree(rejmap);
+	psFree(gradient);
+#endif
+
+	// Stuff into the array
+	inputRej->data[i] = mask;
+
+    }
+
+    psFree(grads);
+    psFree(gradsMask);
+
+    psFree(inCoords);
+    psFree(outCoords);
+
+    return inputRej;
+}
Index: /tags/rel-0_0_1/stac/src/stacScales.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacScales.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacScales.c	(revision 7462)
@@ -0,0 +1,293 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+#define SQUARE(x) ((x)*(x))
+
+#define SAMPLE 10                       // Subsample rate for images
+
+float stacBackground(const psImage *image, // Image for which to get the background
+                     int sample         // Sample in increments of this value
+    )
+{
+    assert(image->type.type == PS_TYPE_F32);
+
+#if 1
+    int size = image->numCols * image->numRows; // Number of pixels in image
+    int numSamples = size / sample;     // Number of samples in image
+    psVector *values = psVectorAlloc(numSamples + 1, PS_TYPE_F32); // Vector containing sub-sample
+    psVector *mask = psVectorAlloc(numSamples + 1, PS_TYPE_U8); // Mask for sample
+
+    int offset = 0;                     // Offset from start of the row
+    int index = 0;                      // Sample number
+    for (int row = 0; row < image->numRows; row++) {
+        // I'd cast this as a "for", but this makes it a bit easier to understand.
+        int col = offset;
+        while (col < image->numCols) {
+            values->data.F32[index] = image->data.F32[row][col];
+            if (isnan(values->data.F32[index])) {
+                mask->data.U8[index] = 1;
+            } else {
+                mask->data.U8[index] = 0;
+            }
+            col += sample;
+            index++;
+        }
+        offset = col - image->numCols;
+    }
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    stats = psVectorStats(stats, values, NULL, mask, 1);
+    float median = stats->sampleMedian;
+    psFree(stats);
+    psFree(values);
+    psFree(mask);
+#else
+    // Will use robust median instead of sampling --- it's supposed to be fast.
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Using a clipped mean because median is SLOW
+    stats->clipSigma = 3.0;
+    stats->clipIter = 5;
+    stats = psImageStats(stats, (psImage*)image, NULL, 0);
+    float median = stats->robustMedian;
+    psFree(stats);
+#endif
+
+    return median;
+}
+
+
+bool stacScales(psVector **scalesPtr,   // Scales to return
+                psVector **offsetsPtr,  // Offsets to return
+                const psArray *images,  // Images on which to measure the scales and offsets
+                const char *starFile,   // File containing coordinates to photometer
+                const char *starMapFile, // Map for coodinates to the common output frame
+                float xMapDiff, float yMapDiff, // Difference from the map to apply (due to size difference)
+                float aper              // Aperture to use for photometry (radius)
+    )
+{
+    assert(scalesPtr);
+    assert(offsetsPtr);
+    assert(images);
+    for (int i = 0; i < images->n; i++) {
+        psImage *image = images->data[i];
+        assert(image->type.type == PS_TYPE_F32);
+    }
+
+    psVector *scales = NULL; // Relative scales between images
+    psVector *offsets = NULL; // Offsets between images (ADU)
+    if (*scalesPtr) {
+        scales = *scalesPtr;
+        assert(scales);
+        assert(scales->n == images->n);
+        assert(scales->type.type == PS_TYPE_F32);
+    } else {
+        scales = psVectorAlloc(images->n, PS_TYPE_F32);
+        *scalesPtr = scales;
+    }
+    if (*offsetsPtr) {
+        offsets = *offsetsPtr;
+        assert(offsets);
+        assert(offsets->n == images->n);
+        assert(offsets->type.type == PS_TYPE_F32);
+    } else {
+        offsets = psVectorAlloc(images->n, PS_TYPE_F32);
+        *offsetsPtr = offsets;
+    }
+
+    // Offsets are easy
+    psTrace("stac.scales", 3, "Getting background levels....\n");
+    double startTime = getTime();
+    for (int i = 0; i < images->n; i++) {
+        offsets->data.F32[i] = stacBackground(images->data[i], SAMPLE);
+        psTrace("stac.scales", 5, "Background in image %d is %f\n", i, offsets->data.F32[i]);
+        double time = getTime();
+        psTrace("stac.scales", 10, "Took %f sec\n", time - startTime);
+        startTime = time;
+    }
+
+    // Now the scales
+    if (starFile == NULL || starMapFile == NULL) {
+        psLogMsg("stac.scales", PS_LOG_INFO,
+                 "No coordinates available to set scales --- assuming all are identical.\n");
+        for (int i = 0; i < images->n; i++) {
+            scales->data.F32[i] = 1.0;
+            psTrace("stac.scales", 5, "Scale for image %d is %f\n", i, scales->data.F32[i]);
+        }
+    } else {
+        // Read star coordinates and map
+        psArray *starCoords = stacReadCoords(starFile); // Array of star coordinates
+        psPlaneTransform *starMap = stacReadMap(starMapFile); // Transformation for star coordinates
+
+        // Transform the stellar positions to match the transformed reference frame
+        psArray *starCoordsTransformed = psArrayAlloc(starCoords->n); // Transformed positions
+        // Fix up difference between map and output frame
+        starMap->x->coeff[0][0] -= xMapDiff;
+        starMap->y->coeff[0][0] -= yMapDiff;
+        for (int i = 0; i < starCoords->n; i++) {
+            starCoordsTransformed->data[i] = psPlaneTransformApply(NULL, starMap, starCoords->data[i]);
+        }
+        psFree(starMap);
+
+        psArray *stars = psArrayAlloc(images->n); // Array of stellar photometry vectors
+        psArray *masks = psArrayAlloc(images->n); // Array of masks for stars
+
+        // Set scales relative to the first image
+        scales->data.F32[0] = 1.0;
+
+        // Iterate over images
+        for (int i = 0; i < scales->n; i++) {
+            psImage *image = images->data[i]; // The image we're working with
+
+            // Do photometry on transformed image i
+            psTrace("stac.scales", 3, "Doing photometry on image %d....\n", i);
+            psVector *photometry = psVectorAlloc(starCoords->n, PS_TYPE_F32); // Photometry of the stars
+            psVector *mask = psVectorAlloc(starCoords->n, PS_TYPE_U8); // Mask for the photometry
+            for (int j = 0; j < starCoordsTransformed->n; j++) {
+                psPlane *coords = starCoordsTransformed->data[j]; // The coordinates of the star
+
+                if (coords->x < aper || coords->y < aper ||
+                    coords->x + aper > image->numCols - 1 ||
+                    coords->y + aper > image->numRows) {
+                    mask->data.U8[j] = 1;
+                } else {
+                    // Sum flux within the aperture
+                    float sum = 0.0;
+                    int numPix = 0;
+                    float aper2 = SQUARE(aper);
+                    for (int y = (int)floorf(coords->y - aper);
+                         y <= (int)ceilf(coords->y + aper); y++) {
+                        for (int x = (int)floorf(coords->x - aper);
+                             x <= (int)ceilf(coords->x + aper); x++) {
+                            if (SQUARE((float)x + 0.5 - coords->x) + SQUARE((float)y + 0.5 - coords->y) <=
+                                aper2) {
+                                sum += image->data.F32[y][x];
+                                numPix++;
+                            }
+                        }
+                    }
+                    // Subtract background, renormalise to account for circular aperture
+                    if (numPix > 0 && sum > 0) {
+                        sum -= offsets->data.F32[i] * (float)numPix;
+                        photometry->data.F32[j] = sum * M_PI * aper2 / (float)numPix;
+                        if (photometry->data.F32[j] > 0 && finite(photometry->data.F32[j])) {
+                            mask->data.U8[j] = 1;
+                            psTrace("stac.scales", 8, "Star at %f,%f --> %f\n", coords->x, coords->y, sum);
+                        } else {
+                            mask->data.U8[j] = 0;
+                        }
+                    } else {
+                        mask->data.U8[j] = 0;
+                    }
+                }
+            }
+            stars->data[i] = photometry;
+            masks->data[i] = mask;
+        }
+        psFree(starCoords);
+        psFree(starCoordsTransformed);
+
+        // Get the scales
+        psVector *ref = stars->data[0]; // The reference photometry
+        psVector *refMask = masks->data[0]; // The reference mask
+#if 0
+        psStats *stats = psStatsAlloc(PS_STAT_CLIPPED_MEAN); // Statistics
+        stats->clipSigma = 2.5;
+        stats->clipIter = 3;
+#else
+        psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN); // Statistics
+#endif
+        for (int i = 1; i < scales->n; i++) {
+            psVector *input = stars->data[i];   // The comparison photometry
+            psVector *inputMask = masks->data[i]; // The comparison mask
+
+            psVector *compare = (psVector*)psBinaryOp(NULL, input, "/", ref);
+            psVector *compareMask = (psVector*)psBinaryOp(NULL, inputMask, "*", refMask);
+            (void)psBinaryOp(compareMask, psScalarAlloc(1, PS_TYPE_U8), "-", compareMask);
+
+#if 0
+            psTrace("stac.scales", 9, "Getting scale for image %d...\n", i);
+            for (int j = 0; j < compare->n; j++) {
+                if (compareMask->data.U8[j] & 1) {
+                    psTrace("stac.scales", 9, "Bad star %d: %f %f --> %f %d\n", j, input->data.F32[j],
+                            ref->data.F32[j], compare->data.F32[j], compareMask->data.U8[j]);
+                } else {
+                    psTrace("stac.scales", 9, "Good star %d: %f %f --> %f %d\n", j, input->data.F32[j],
+                            ref->data.F32[j], compare->data.F32[j], compareMask->data.U8[j]);
+                }
+            }
+#endif
+
+            psVectorStats(stats, compare, NULL, compareMask, 1);
+
+#if 0
+            scales->data.F32[i] = stats->clippedMean;
+#else
+            scales->data.F32[i] = stats->sampleMedian;
+#endif
+            psTrace("stac.scales", 5, "Scale for image %d is %f\n", i, scales->data.F32[i]);
+            psFree(compare);
+            psFree(compareMask);
+        }
+        psFree(stats);
+
+        psFree(stars);
+        psFree(masks);
+    }
+
+    return true;
+}
+
+
+bool stacRescale(psArray *images,       // Images to rescale
+                 psArray *errImages,    // Variance images to rescale
+                 const psImage *mask,   // Mask for pixels to scale
+                 const psVector *scales,// Scales for images
+                 const psVector *offsets // Offsets for images
+    )
+{
+    assert(images);
+    assert(scales);
+    assert(offsets);
+    assert(images->n == scales->n);
+    assert(images->n == offsets->n);
+    assert(!errImages || errImages->n == images->n);
+    assert(scales->type.type == PS_TYPE_F32);
+    assert(offsets->type.type == PS_TYPE_F32);
+    for (int i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // Image of interest
+        assert(image->type.type == PS_TYPE_F32);
+        assert(scales->data.F32[i] != 0);
+        if (mask) {
+            assert(mask->type.type == PS_TYPE_U8);
+            assert(image->numCols == mask->numCols && image->numRows == mask->numRows);
+        }
+        if (errImages) {
+            psImage *errImage = errImages->data[i];
+            assert(errImage->type.type == PS_TYPE_F32);
+            assert(errImage->numCols == image->numCols && errImage->numRows == image->numRows);
+        }
+    }
+
+    for (int i = 0; i < images->n; i++) {
+        psImage *image = images->data[i]; // Image to rescale
+        psImage *errImage = NULL;       // Variance image to rescale
+        if (errImages) {
+            errImage = errImages->data[i];
+        }
+        float scale = scales->data.F32[i]; // Scale to use
+        float offset = offsets->data.F32[i]; // Offset to use
+        for (int y = 0; y < image->numRows; y++) {
+            for (int x = 0; x < image->numCols; x++) {
+                if (!mask || mask->data.F32[y][x]) {
+                    image->data.F32[y][x] = (image->data.F32[y][x] - offset) / scale;
+                    if (errImage) {
+                        errImage->data.F32[y][x] /= SQUARE(scale);
+                    }
+                }
+            }
+        }
+    }
+
+    return true;
+}
Index: /tags/rel-0_0_1/stac/src/stacSize.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacSize.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacSize.c	(revision 7462)
@@ -0,0 +1,116 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+bool stacSize(int *outnx, int *outny,	// Size of output image (returned)
+	      float *xMapDiff, float *yMapDiff, // Difference applied to maps
+	      const psArray *images,	// Input images
+	      psArray *maps		// Transformation maps
+    )
+{
+    int nImages = images->n;		// Number of input images
+    assert(maps->n == nImages);
+    assert(outnx && outny);		// Must be able to write to these
+
+    psPlane *inCoord = psAlloc(sizeof(psPlane)); // Input coordinates
+    psPlane *outCoord = psAlloc(sizeof(psPlane)); // Output coordinates
+
+    // Initial "guess" at limits
+    float xMin = INFINITY;
+    float xMax = 0.0;
+    float yMin = INFINITY;
+    float yMax = 0.0;
+
+    for (int i = 0; i < nImages; i++) {
+	psImage *image = images->data[i]; // The image
+	psPlaneTransform *map = maps->data[i]; // The map
+
+	psTrace("stac.size", 4, "Image %d:\n", i);
+
+	// Lower left corner
+	inCoord->x = 0.0;
+	inCoord->y = 0.0;
+	(void)psPlaneTransformApply(outCoord, map, inCoord);
+	if (outCoord->x < xMin) {
+	    xMin = outCoord->x;
+	} else if (outCoord->x > xMax) {
+	    xMax = outCoord->x;
+	}
+	if (outCoord->y < yMin) {
+	    yMin = outCoord->y;
+	} else if (outCoord->y > yMax) {
+	    yMax = outCoord->y;
+	}
+	psTrace("stac.size", 4, "Lower left: %f %f\n", outCoord->x, outCoord->y);
+
+	// Lower right corner
+	inCoord->x = image->numCols;
+	(void)psPlaneTransformApply(outCoord, map, inCoord);
+	if (outCoord->x < xMin) {
+	    xMin = outCoord->x;
+	} else if (outCoord->x > xMax) {
+	    xMax = outCoord->x;
+	}
+	if (outCoord->y < yMin) {
+	    yMin = outCoord->y;
+	} else if (outCoord->y > yMax) {
+	    yMax = outCoord->y;
+	}
+	psTrace("stac.size", 4, "Lower right: %f %f\n", outCoord->x, outCoord->y);
+
+	// Upper right corner
+	inCoord->x = 0.0;
+	inCoord->y = image->numRows;
+	(void)psPlaneTransformApply(outCoord, map, inCoord);
+	if (outCoord->x < xMin) {
+	    xMin = outCoord->x;
+	} else if (outCoord->x > xMax) {
+	    xMax = outCoord->x;
+	}
+	if (outCoord->y < yMin) {
+	    yMin = outCoord->y;
+	} else if (outCoord->y > yMax) {
+	    yMax = outCoord->y;
+	}
+	psTrace("stac.size", 4, "Upper right: %f %f\n", outCoord->x, outCoord->y);
+
+	// Upper left corner
+	inCoord->x = image->numCols;
+	(void)psPlaneTransformApply(outCoord, map, inCoord);
+	if (outCoord->x < xMin) {
+	    xMin = outCoord->x;
+	} else if (outCoord->x > xMax) {
+	    xMax = outCoord->x;
+	}
+	if (outCoord->y < yMin) {
+	    yMin = outCoord->y;
+	} else if (outCoord->y > yMax) {
+	    yMax = outCoord->y;
+	}
+	psTrace("stac.size", 4, "Upper left: %f %f\n", outCoord->x, outCoord->y);
+    }
+
+    // Tweak the maps to account for the offset
+    if (xMapDiff != NULL) {
+	*xMapDiff = floorf(xMin);
+    }
+    if (yMapDiff != NULL) {
+	*yMapDiff = floorf(yMin);
+    }
+    for (int i = 0; i < nImages; i++) {
+	psPlaneTransform *map = maps->data[i]; // The map of interest
+	map->x->coeff[0][0] -= floorf(xMin);
+	map->y->coeff[0][0] -= floorf(yMin);
+    }
+
+    *outnx = (int)ceilf(xMax - xMin);
+    *outny = (int)ceilf(yMax - yMin);
+
+    psTrace("stac.size", 1, "Output size is to be %dx%d\n", *outnx, *outny);
+
+    psFree(inCoord);
+    psFree(outCoord);
+
+    return true;
+}
Index: /tags/rel-0_0_1/stac/src/stacTime.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacTime.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacTime.c	(revision 7462)
@@ -0,0 +1,12 @@
+#include <stdio.h>
+#include <sys/time.h>
+
+double getTime(void)
+/* Gets the current time.  Got this from Nick Kaiser's fetchpix.c */
+{
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return(tv.tv_sec + 1.e-6 * tv.tv_usec);
+}
+
+
Index: /tags/rel-0_0_1/stac/src/stacTransform.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacTransform.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacTransform.c	(revision 7462)
@@ -0,0 +1,229 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+#define SQUARE(x) ((x)*(x))
+#define MIN(x,y) (((x) > (y)) ? (y) : (x))
+#define MAX(x,y) (((x) > (y)) ? (x) : (y))
+
+// Hacked the original ps_ImagePixelInterpolateBILINEAR_F32 to add variances
+// i.e., to square the fractions when combining.
+inline psF64 p_psImageErrorInterpolateBILINEAR_F32(const psImage* input,
+                                                   float x,
+                                                   float y,
+                                                   const psImage* mask,
+                                                   unsigned int maskVal,
+                                                   psF64 unexposedValue)
+{
+    double floorX = floor((psF64)(x) - 0.5);
+    double floorY = floor((psF64)(y) - 0.5);
+    psF64 fracX = x - 0.5 - floorX;
+    psF64 fracY = y - 0.5 - floorY;
+    int intFloorX = (int) floorX;
+    int intFloorY = (int) floorY;
+    int lastX = input->numCols - 1;
+    int lastY = input->numRows - 1;
+    psF32 V00 = 0.0;
+    psF32 V01 = 0.0;
+    psF32 V10 = 0.0;
+    psF32 V11 = 0.0;
+    bool valid00 = false;
+    bool valid01 = false;
+    bool valid10 = false;
+    bool valid11 = false;
+
+    if (intFloorY >= 0 && intFloorY <= lastY) {
+        if (intFloorX >= 0 && intFloorX <= lastX) {
+            V00 = input->data.F32[intFloorY][intFloorX];
+            valid00 = true;
+        }
+        if (intFloorX >= -1 && intFloorX < lastX) {
+            V10 = input->data.F32[intFloorY][intFloorX+1];
+            valid10 = true;
+        }
+    }
+    if (intFloorY >= -1 && intFloorY < lastY) {
+        if (intFloorX >= 0 && intFloorX <= lastX) {
+            V01 = input->data.F32[intFloorY+1][intFloorX];
+            valid01 = true;
+        }
+        if (intFloorX >= -1 && intFloorX < lastX) {
+            V11 = input->data.F32[intFloorY+1][intFloorX+1];
+            valid11 = true;
+        }
+    }
+
+    /* cover likely case of all pixels being valid more efficiently */
+    if (valid00 && valid10 && valid01 && valid11) {
+        /* formula from the ADD */
+        return V00*SQUARE((1.0-fracX)*(1.0-fracY)) + V10*SQUARE(fracX*(1.0-fracY)) +
+            V01*SQUARE(fracY*(1.0-fracX)) + V11*SQUARE(fracX*fracY);
+    }
+
+    /* OK, at least one pixel is not valid - need to do it piecemeal */
+
+    psF64 V0 = 0.0;
+    bool valid0 = true;
+    if (valid00 && valid10) {
+        V0 = V00*SQUARE(1-fracX)+V10*SQUARE(fracX);
+    } else if (valid00) {
+        V0 = V00;
+    } else if (valid10) {
+        V0 = V10;
+    } else {
+        valid0 = false;
+    }
+
+    psF64 V1 = 0.0;
+    bool valid1 = true;
+    if (valid01 && valid11) {
+        V1 = V01*SQUARE(1-fracX)+V11*SQUARE(fracX);
+    } else if (valid01) {
+        V1 = V01;
+    } else if (valid11) {
+        V1 = V11;
+    } else {
+        valid1 = false;
+    }
+
+    if (valid0 && valid1) {
+        return ( V0*SQUARE(1-fracY) + V1*SQUARE(fracY) );
+    } else if (valid0) {
+        return V0;
+    } else if (valid1) {
+        return V1;
+    }
+
+    return unexposedValue;
+}
+
+
+
+bool stacTransform(psArray **outputs,   // Transformed images for output
+                   psArray **outErrors, // Transformed error images for output
+                   const psArray *images, // Array of images to be transformed
+                   const psArray *maps, // Array of polynomials that do the transformation
+                   const psArray *errors, // Array of error images to be transformed
+                   const psArray *masks, // Masks of input images
+                   const psImage *region, // Region of interest for transformation
+                   const psVector *scales, // Relative scales
+                   const psVector *offsets, // Relative offsets
+                   int outnx, int outny // Size of output images
+    )
+{
+    int nImages = images->n;            // Number of images
+
+    // Check input sizes
+    assert(images->n == maps->n);
+    assert(!errors || (images->n == errors->n));
+    assert(!scales || scales->n == images->n);
+    assert(!offsets || offsets->n == images->n);
+    assert(!scales || scales->type.type == PS_TYPE_F32);
+    assert(!offsets || offsets->type.type == PS_TYPE_F32);
+
+    // Allocate the output images if required, otherwise check the number
+    assert(!*outputs || (*outputs)->n == nImages);
+    if (*outputs == NULL) {
+        *outputs = psArrayAlloc(nImages);
+        psTrace("stac.transform", 5, "Allocating space for transformed images, %dx%d\n", outnx, outny);
+        for (int i = 0; i < nImages; i++) {
+            (*outputs)->data[i] = psImageAlloc(outnx, outny, PS_TYPE_F32);
+            psImageInit((*outputs)->data[i], 0.0);
+        }
+    }
+
+    // Allocate the output error images, if required, otherwise check the number
+    assert(!errors || ! *outErrors || errors->n == (*outErrors)->n);
+    if (errors && (*outErrors == NULL)) {
+        *outErrors = psArrayAlloc(errors->n);
+        psTrace("stac.transform", 5, "Allocating space for transformed error images, %dx%d\n", outnx, outny);
+        for (int i = 0; i < nImages; i++) {
+            (*outErrors)->data[i] = psImageAlloc(outnx, outny, PS_TYPE_F32);
+        }
+    }
+
+    // Check the masks, if specified
+    assert(!masks || masks->n == nImages);
+    if (masks != NULL) {
+        for (int i = 0; i < nImages; i++) {
+            psImage *image = images->data[i];
+            psImage *mask = masks->data[i];
+            assert(mask->numRows == image->numRows && mask->numCols == image->numCols);
+        }
+    }
+
+
+    // Stuff for the transformations
+    psPlane *detector = psAlloc(sizeof(psPlane)); // Coordinates on the detector
+    psPlane *sky = psAlloc(sizeof(psPlane)); // Coordinates on the sky
+
+
+    // Iterate over the images
+    for (int n = 0; n < nImages; n++) {
+        psTrace("stac.transform", 1, "Transforming image %d....\n",n);
+
+        // Pull out the various stuff we're working on
+        psImage *image = images->data[n]; // The input image
+        psPlaneTransform *map = maps->data[n]; // The map
+        psImage *outImage = (*outputs)->data[n]; // The output image
+        psImage *error = NULL; // The error image
+        psImage *outError = NULL; // The output error image
+        if (errors) {
+            error = errors->data[n];
+            outError = (*outErrors)->data[n];
+        }
+        float offset = 0.0;             // Relative offset
+        float scale = 1.0;              // Relative scale
+        if (offsets) {
+            offset = offsets->data.F32[n];
+        }
+        if (scales) {
+            scale = scales->data.F32[n];
+        }
+
+        // Mask
+        psImage *mask = NULL;
+        if (masks != NULL) {
+            mask = masks->data[n];
+        }
+
+        // Iterate over the output image pixels
+        for (int y = 0; y < outny; y++) {
+            for (int x = 0; x < outnx; x++) {
+                // Only transform those pixels requested
+                if (!region || (region && region->data.U8[y][x])) {
+                    // Transform!
+                    sky->x = (double)x + 0.5;
+                    sky->y = (double)y + 0.5;
+                    (void)psPlaneTransformApply(detector, map, sky);
+
+                    // Change PS_INTERPOLATE_BILINEAR to best available technique.
+                    outImage->data.F32[y][x] = (psF32)psImagePixelInterpolate(image, detector->x,
+                                                                              detector->y, mask, 1, NAN,
+                                                                              PS_INTERPOLATE_BILINEAR);
+                    if (error) {
+                        // Error is actually the variance
+                        outError->data.F32[y][x] = (psF32)p_psImageErrorInterpolateBILINEAR_F32(error,
+                                                                                                detector->x,
+                                                                                                detector->y,
+                                                                                                mask, 1, NAN);
+                    }
+
+                    outImage->data.F32[y][x] = (outImage->data.F32[y][x] - offset) / scale;
+                    outImage->data.F32[y][x] = outImage->data.F32[y][x] / SQUARE(scale);
+
+                } // Pixels of interest
+
+            }
+        } // Iterating over output pixels
+
+    } // Iterating over images
+
+    // Done with transformations
+    psFree(detector);
+    psFree(sky);
+
+    return true;
+}
+
Index: /tags/rel-0_0_1/stac/src/stacWrite.c
===================================================================
--- /tags/rel-0_0_1/stac/src/stacWrite.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/stacWrite.c	(revision 7462)
@@ -0,0 +1,77 @@
+#include <stdio.h>
+#include <assert.h>
+#include "pslib.h"
+#include "stac.h"
+
+bool stacWriteMap(const char *mapName,  // Filename to write to
+                  psPlaneTransform *map // Map to write
+    )
+{
+    assert(mapName);
+
+    FILE *mapFile = fopen(mapName, "w");
+    if (!mapFile) {
+        fprintf(stderr, "Unable to open map file: %s\n", mapName);
+        return false;
+    }
+
+    psPolynomial2D *xMap = map->x;      // x transform
+    psPolynomial2D *yMap = map->y;      // y transform
+
+    // A crucial limitation of the current system --- the order of each polynomial must be the same
+    assert(xMap->nX == xMap->nY && yMap->nX == yMap->nY && xMap->nX == yMap->nX);
+    int order = xMap->nX;       // The polynomial order
+    fprintf(mapFile, "%d\n", order);
+
+    // x coefficients
+    for (int k = 0; k <= order; k++) {
+        for (int j = 0; j <= k; j++) {
+            int i = k - j;
+            if (xMap->mask[i][j]) {
+                fprintf(mapFile, "0.0 ");
+            } else {
+                fprintf(mapFile, "%g ", xMap->coeff[i][j]);
+            }
+        }
+    }
+    fprintf(mapFile, "\n");
+
+    // y coefficients
+    for (int k = 0; k <= order; k++) {
+        for (int j = 0; j <= k; j++) {
+            int i = k - j;
+            if (yMap->mask[i][j]) {
+                fprintf(mapFile, "0.0 ");
+            } else {
+                fprintf(mapFile, "%g ", yMap->coeff[i][j]);
+            }
+        }
+    }
+    fprintf(mapFile, "\n");
+
+    fclose(mapFile);
+
+    return true;
+}
+
+
+bool stacWriteMaps(const psArray *names, // Filenames of the input images (will add ".map")
+                   const psArray *maps  // Maps to write
+    )
+{
+    assert(names);
+    assert(maps);
+    assert(names->n == maps->n);
+
+    for (int i = 0; i < names->n; i++) {
+        char mapName[MAXCHAR];          // Filename of error image
+        sprintf(mapName, "%s.map", (const char*)names->data[i]);
+        if (!stacWriteMap(mapName, maps->data[i])) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
Index: /tags/rel-0_0_1/stac/src/sum.c
===================================================================
--- /tags/rel-0_0_1/stac/src/sum.c	(revision 7462)
+++ /tags/rel-0_0_1/stac/src/sum.c	(revision 7462)
@@ -0,0 +1,82 @@
+// Brain-dead simple stack of multiple frames
+// Makes no effort to zap bad pixels, or rescale images.
+// Assumes that images are already registered
+
+#include <stdio.h>
+#include "pslib.h"
+
+int main(int argc, char *argv[])
+{
+    const char *outputName = argv[1];   // Output file name
+    psArray *inputNames = psArrayAlloc(argc-2);
+    for (int i = 2; i < argc; i++) {
+        inputNames->data[i-2] = psStringCopy(argv[i]);
+    }
+
+    int numCols = 0;                    // Number of columns
+    int numRows = 0;                    // Number of rows
+    psImage *output = NULL;             // Output image
+    psImage *scale = NULL;              // Scale image
+    psMetadata *header = NULL;          // FITS header for output image
+    for (int i = 0; i < inputNames->n; i++) {
+        psFits *fits = psFitsOpen(inputNames->data[i], "r"); // FITS file
+        psRegion readRegion = {0, 0, 0, 0}; // Region to read
+        psImage *image = psFitsReadImage(NULL, fits, readRegion, 0);
+        if (numCols == 0 && numRows == 0) {
+            // First run through --- set the size, initialise the output, read the header
+            numCols = image->numCols;
+            numRows = image->numRows;
+            output = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            scale = psImageAlloc(numCols, numRows, PS_TYPE_U8);
+            for (int y = 0; y < numCols; y++) {
+                for (int x = 0; x < numRows; x++) {
+                    output->data.F32[y][x] = 0.0;
+                    scale->data.U8[y][x] = 0;
+                }
+            }
+            header = psFitsReadHeader(NULL, fits);
+        } else if (image->numCols != numCols || image->numRows != numRows) {
+            psError(PS_ERR_IO, true, "Input images have different dimensions.  %s is %dx%d, but should be "
+                    "%dx%d\n", inputNames->data[i], image->numCols, image->numRows, numCols, numRows);
+            return EXIT_FAILURE;
+        }
+        psFitsClose(fits);
+
+        // Convert the type, if necessary
+        if (image->type.type != PS_TYPE_F32) {
+            psImage *temp = psImageCopy(NULL, image, PS_TYPE_F32);
+            psFree(image);
+            image = temp;
+        }
+
+        // Add in
+        for (int y = 0; y < numCols; y++) {
+            for (int x = 0; x < numRows; x++) {
+                if (image->data.F32[y][x] > 0) {
+                    output->data.F32[y][x] += image->data.F32[y][x];
+                    scale->data.U8[y][x]++;
+                }
+            }
+        }
+
+        psFree(image);
+    }
+    psFree(inputNames);
+
+    for (int y = 0; y < numCols; y++) {
+        for (int x = 0; x < numRows; x++) {
+            if (scale->data.U8[y][x] > 0) {
+                output->data.F32[y][x] /= (float)scale->data.U8[y][x];
+            } else {
+                output->data.F32[y][x] = 0.0;
+            }
+        }
+    }
+
+    psFits *fits = psFitsOpen(outputName, "w");
+    (void)psFitsWriteImage(fits, header, output, 0);
+    psFitsClose(fits);
+    psFree(output);
+    // Pau.
+    return EXIT_SUCCESS;
+}
