Index: trunk/psModules/configure.ac
===================================================================
--- trunk/psModules/configure.ac	(revision 6529)
+++ trunk/psModules/configure.ac	(revision 6872)
@@ -8,20 +8,8 @@
 AM_MAINTAINER_MODE
 
-dnl handle debug building
-AC_ARG_ENABLE(optimize,
-  [AS_HELP_STRING(--enable-optimize,enable compiler optimization)],
-  [AC_MSG_RESULT(compile optimization enabled)
-   CFLAGS="${CFLAGS=} -O2"],
-  [AC_MSG_RESULT([compile optimization disabled])
-   CFLAGS="${CFLAGS=} -O0 -g"]
-)
-
-dnl handle profiler building
-AC_ARG_ENABLE(profile,
-  [AS_HELP_STRING(--enable-profile,enable compiler profiler information inclusion)],
-  [AC_MSG_RESULT(compile optimization enabled)
-   CFLAGS="${CFLAGS=} -fprofile-arcs -ftest-coverage -pg"]
-)
-
+dnl otherise AC_PROG_CC will default CFLAGS to "-g -02"
+if test -z ${CFLAGS} ; then
+  CFLAGS=""
+fi
 AC_SUBST([CFLAGS])
 
@@ -29,8 +17,7 @@
 AC_PROG_CC
 dnl FIXME document why GNU_SOURCE is being set
-dnl AC_GNU_SOURCE
+AC_GNU_SOURCE
 AC_PROG_INSTALL
 AM_PROG_LIBTOOL
-AC_SYS_LARGEFILE
 
 AC_PREFIX_DEFAULT([`pwd`])
@@ -62,12 +49,30 @@
 
 SRCPATH='${top_srcdir}/src'
-SRCDIRS="astrom camera config detrend imcombine imsubtract objects photom"
+SRCDIRS="astrom config detrend imcombine imsubtract objects pslib"
 # escape two escapes at this level so \\ gets passed to the shell and \ to perl
 SRCINC=`echo "${SRCDIRS=}" | ${PERL} -pe "s|(\w+)|-I\\\\${SRCPATH=}/\1|g"`
 SRCINC="-I${SRCPATH=} ${SRCINC=}"
 SRCSUBLIBS=`echo "${SRCDIRS=}" | ${PERL} -pe "s|(\w+)|\1/libpsmodule\1.la|g"`
-AC_SUBST([SRCSUBLIBS],${SRCSUBLIBS=})
-AC_SUBST([SRCINC],${SRCINC=})
+AC_SUBST(SRCSUBLIBS,${SRCSUBLIBS=})
+AC_SUBST(SRCINC,${SRCINC=})
 AC_SUBST([SRCDIRS],${SRCDIRS=})
+
+dnl handle debug building
+AC_ARG_ENABLE(optimize,
+  [AS_HELP_STRING(--enable-optimize,enable compiler optimization)],
+  [AC_MSG_RESULT(compile optimization enabled)
+   CFLAGS="${CFLAGS=} -O2"],
+  [AC_MSG_RESULT([compile optimization disabled])
+   CFLAGS="${CFLAGS=} -O0 -g"]
+)
+
+dnl handle profiler building
+AC_ARG_ENABLE(profile,
+  [AS_HELP_STRING(--enable-profile,enable compiler profiler information inclusion)],
+  [AC_MSG_RESULT(compile optimization enabled)
+   CFLAGS="${CFLAGS=} -pg"]
+)
+
+dnl CFLAGS="${CFLAGS=} -fprofile-arcs -ftest-coverage -pg"]
 
 dnl doxygen -------------------------------------------------------------------
@@ -75,4 +80,8 @@
 AC_CHECK_PROG([doxygen], [doxygen], [true], [false])
 AM_CONDITIONAL(DOXYGEN, test x$doxygen = xtrue)
+
+dnl libjpeg -------------------------------------------------------------------
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,[],[AC_MSG_ERROR([jpeg library not found.])])
 
 dnl pslib ---------------------------------------------------------------------
@@ -82,5 +91,5 @@
 
 if test -z ${PSLIB_CONFIG} ; then
-  PKG_CHECK_MODULES([PSLIB], [pslib >= 0.10.0])
+  PKG_CHECK_MODULES([PSLIB], [pslib >= 0.9.0])
 else
   AC_CHECK_FILE($PSLIB_CONFIG,[],
@@ -105,5 +114,4 @@
   src/Makefile
   src/astrom/Makefile
-  src/camera/Makefile
   src/config/Makefile
   src/detrend/Makefile
@@ -111,8 +119,7 @@
   src/imsubtract/Makefile
   src/objects/Makefile
-  src/photom/Makefile
+  src/pslib/Makefile
   test/Makefile
   test/astrom/Makefile
-  test/camera/Makefile
   test/config/Makefile
   test/detrend/Makefile
@@ -120,5 +127,5 @@
   test/imsubtract/Makefile
   test/objects/Makefile
-  test/photom/Makefile
+  test/pslib/Makefile
   Doxyfile
   psmodule-config
Index: trunk/psModules/src/Makefile.am
===================================================================
--- trunk/psModules/src/Makefile.am	(revision 6529)
+++ trunk/psModules/src/Makefile.am	(revision 6872)
@@ -7,3 +7,6 @@
 libpsmodule_la_DEPENDENCIES = $(SRCSUBLIBS)
 
+include_HEADERS = \
+	psmodules.h
+
 EXTRA_DIST = psErrorCodes.dat
Index: trunk/psModules/src/astrom/Makefile.am
===================================================================
--- trunk/psModules/src/astrom/Makefile.am	(revision 6529)
+++ trunk/psModules/src/astrom/Makefile.am	(revision 6872)
@@ -1,12 +1,50 @@
 noinst_LTLIBRARIES = libpsmoduleastrom.la
 
-libpsmoduleastrom_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
+libpsmoduleastrom_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS) -I../pslib/
 libpsmoduleastrom_la_LDFLAGS  = -release $(PACKAGE_VERSION)
 libpsmoduleastrom_la_SOURCES  = \
-    pmAstrometry.c \
-    pmAstrometryObjects.c
+	pmFPA.c \
+	pmFPAConstruct.c \
+	pmFPACopy.c \
+	pmFPAMaskWeight.c \
+	pmFPARead.c \
+	pmFPAUtils.c \
+	pmFPAWrite.c \
+	pmHDU.c \
+	pmHDUUtils.c \
+	pmReadout.c \
+	pmConcepts.c \
+	pmConceptsRead.c \
+	pmConceptsWrite.c \
+	pmConceptsStandard.c \
+	pmFPA_JPEG.c \
+	pmFPAview.c \
+	pmFPAfile.c
+
+#	pmFPAAstrometry.c
+#	pmAstrometryObjects.c
+#	pmChipMosaic.c
 
 psmoduleincludedir = $(includedir)
 psmoduleinclude_HEADERS = \
-    pmAstrometry.h \
-    pmAstrometryObjects.h
+	pmFPA.h \
+	pmFPAConstruct.h \
+	pmFPACopy.h \
+	pmFPAMaskWeight.h \
+	pmFPARead.h \
+	pmFPAUtils.h \
+	pmFPAWrite.h \
+	pmHDU.h \
+	pmHDUUtils.h \
+	pmReadout.h \
+	pmConcepts.h \
+	pmConceptsRead.h \
+	pmConceptsWrite.h \
+	pmConceptsStandard.h \
+	pmFPA_JPEG.h \
+	pmFPAview.h \
+	pmFPAfile.h
+
+#	pmFPAAstrometry.h
+#	pmAstrometryObjects.h
+#	pmChipMosaic.h 
Index: trunk/psModules/src/astrom/pmAstrometry.c
===================================================================
--- trunk/psModules/src/astrom/pmAstrometry.c	(revision 6529)
+++ trunk/psModules/src/astrom/pmAstrometry.c	(revision 6872)
@@ -10,9 +10,9 @@
 * XXX: We should review the extent of the warning messages on these functions
 * when the transformations are not successful.
-* 
+*
 * XXX: Should we implement non-linear cell->chip transforms?
-* 
-*  @version $Revision: 1.12 $ $Name: not supported by cvs2svn $
-*  @date $Date: 2006-01-26 21:10:50 $
+*
+*  @version $Revision: 1.13 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
@@ -25,5 +25,7 @@
 #include <math.h>
 #include "pslib.h"
+
 #include "pmAstrometry.h"
+#include "pmConcepts.h"
 
 /*****************************************************************************
@@ -60,21 +62,11 @@
         psFree(readout->mask);
         psFree(readout->weight);
-        //
-        // XXX: Not sure if this is the right way to do things.  Currently the psListAdd()
-        // increase the memory reference counter to the list data.  So, we
-        // iterate through the list, and decrement the reference counters.
-        //
-        if (1) {
-            if ((readout->bias != NULL) && (readout->bias->head != NULL)) {
-                psListElem *tmpElem = (psListElem *) readout->bias->head;
-                while (NULL != tmpElem) {
-                    psMemDecrRefCounter((psImage *) tmpElem->data);
-                    tmpElem = tmpElem->next;
-                }
-            }
-        }
-        psFree(readout->bias);
         psFree(readout->analysis);
+        #if 0
+
         psFree(readout->parent);
+        #endif
+
+        readout->parent = NULL;
     }
 }
@@ -88,8 +80,11 @@
         psFree(cell->concepts);
         psFree(cell->analysis);
+        psFree(cell->camera);
         //
         // Set the parent to NULL in all cell->readouts before psFree(cell->readouts)
         // in order to avoid memory reference counter problems.
         //
+        #if 0
+
         for (psS32 i = 0 ; i < cell->readouts->n ; i++) {
             pmReadout *tmpReadout = (pmReadout *) cell->readouts->data[i];
@@ -99,7 +94,12 @@
             }
         }
+        psFree(cell->parent);
+        #endif
+
+        cell->parent = NULL;
+
         psFree(cell->readouts);
-        psFree(cell->parent);
         psFree(cell->hdu);
+
     }
 }
@@ -116,4 +116,6 @@
         // in order to avoid memory reference counter problems.
         //
+        #if 0
+
         for (psS32 i = 0 ; i < chip->cells->n ; i++) {
             pmCell *tmpCell = (pmCell *) chip->cells->data[i];
@@ -123,6 +125,9 @@
             }
         }
+        psFree(chip->parent);
+        #endif
+
+        chip->parent = NULL;
         psFree(chip->cells);
-        psFree(chip->parent);
         psFree(chip->hdu);
     }
@@ -143,4 +148,6 @@
         // in order to avoid memory reference counter problems.
         //
+        #if 0
+
         for (psS32 i = 0 ; i < fpa->chips->n ; i++) {
             pmChip *tmpChip = (pmChip *) fpa->chips->data[i];
@@ -150,4 +157,5 @@
             }
         }
+        #endif
         psFree(fpa->chips);
         psFree(fpa->hdu);
@@ -156,17 +164,28 @@
 }
 
+void p_pmHDUFree(p_pmHDU *hdu)
+{
+    if (hdu) {
+        psFree(hdu->extname);
+        psFree(hdu->header);
+        psFree(hdu->images);
+        psFree(hdu->masks);
+        psFree(hdu->weights);
+    }
+}
+
 // XXX: Verify these default values for row0, col0, rowBins, colBins
+// PAP: These values may disappear in the future in favour of values in parent->concepts?
 pmReadout *pmReadoutAlloc(pmCell *cell)
 {
     pmReadout *tmpReadout = (pmReadout *) psAlloc(sizeof(pmReadout));
 
-    tmpReadout->col0 = -1;
-    tmpReadout->row0 = -1;
-    tmpReadout->colBins = -1;
-    tmpReadout->rowBins = -1;
+    tmpReadout->col0 = 0;
+    tmpReadout->row0 = 0;
+    tmpReadout->colBins = 0;
+    tmpReadout->rowBins = 0;
     tmpReadout->image = NULL;
     tmpReadout->mask = NULL;
     tmpReadout->weight = NULL;
-    tmpReadout->bias = NULL;
     tmpReadout->analysis = psMetadataAlloc();
     tmpReadout->parent = cell;
@@ -179,13 +198,14 @@
 
 // XXX: Verify these default values for row0, col0.
+// PAP: These values may disappear in the future in favour of values in the "concepts"?
 pmCell *pmCellAlloc(
     pmChip *chip,
-    psMetadata *cameradata,
-    psString name)
+    psMetadata *cameraData,
+    const char *name)
 {
     pmCell *tmpCell = (pmCell *) psAlloc(sizeof(pmCell));
 
-    tmpCell->col0 = -1;
-    tmpCell->row0 = -1;
+    tmpCell->col0 = 0;
+    tmpCell->row0 = 0;
     tmpCell->toChip = NULL;
     tmpCell->toFPA = NULL;
@@ -196,6 +216,6 @@
         psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CELL.NAME to metadata.\n");
     }
-    tmpCell->camera = cameradata;
-    tmpCell->analysis = NULL;
+    tmpCell->camera = psMemIncrRefCounter(cameraData);
+    tmpCell->analysis = psMetadataAlloc();
     tmpCell->readouts = psArrayAlloc(0);
     tmpCell->parent = chip;
@@ -203,6 +223,9 @@
         chip->cells = psArrayAdd(chip->cells, 1, (psPtr) tmpCell);
     }
-    tmpCell->valid = false;
+    tmpCell->process = true;            // All cells are processed by default
+    tmpCell->exists = false;            // Not yet read in
     tmpCell->hdu = NULL;
+
+    pmConceptsBlankCell(tmpCell);
 
     psMemSetDeallocator(tmpCell, (psFreeFunc) cellFree);
@@ -211,12 +234,13 @@
 
 // XXX: Verify these default values for row0, col0.
+// PAP: row0, col0 may disappear in the future in favour of storing values in the "concepts".
 pmChip *pmChipAlloc(
     pmFPA *fpa,
-    psString name)
+    const char *name)
 {
     pmChip *tmpChip = (pmChip *) psAlloc(sizeof(pmChip));
 
-    tmpChip->col0 = -1;
-    tmpChip->row0 = -1;
+    tmpChip->col0 = 0;
+    tmpChip->row0 = 0;
     tmpChip->toFPA = NULL;
     tmpChip->fromFPA = NULL;
@@ -226,5 +250,5 @@
         psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CHIP.NAME to metadata.\n");
     }
-    tmpChip->analysis = NULL;
+    tmpChip->analysis = psMetadataAlloc();
     tmpChip->cells = psArrayAlloc(0);
     tmpChip->parent = fpa;
@@ -232,6 +256,9 @@
         fpa->chips = psArrayAdd(fpa->chips, 1, (psPtr) tmpChip);
     }
-    tmpChip->valid = false;
+    tmpChip->process = true;            // Work on all chips, by default
+    tmpChip->exists = false;            // Not read in yet
     tmpChip->hdu = NULL;
+
+    pmConceptsBlankChip(tmpChip);
 
     psMemSetDeallocator(tmpChip, (psFreeFunc) chipFree);
@@ -248,11 +275,27 @@
     tmpFPA->concepts = psMetadataAlloc();
     tmpFPA->analysis = NULL;
-    tmpFPA->camera = camera;
+    tmpFPA->camera = psMemIncrRefCounter((psPtr)camera);
     tmpFPA->chips = psArrayAlloc(0);
     tmpFPA->hdu = NULL;
     tmpFPA->phu = NULL;
 
+    pmConceptsBlankFPA(tmpFPA);
+
     psMemSetDeallocator(tmpFPA, (psFreeFunc) FPAFree);
     return(tmpFPA);
+}
+
+p_pmHDU *p_pmHDUAlloc(const char *extname)
+{
+    p_pmHDU *hdu = psAlloc(sizeof(p_pmHDU));
+    psMemSetDeallocator(hdu, (psFreeFunc)p_pmHDUFree);
+
+    hdu->extname = psStringCopy(extname);
+    hdu->header = NULL;
+    hdu->images = NULL;
+    hdu->masks = NULL;
+    hdu->weights = NULL;
+
+    return hdu;
 }
 
@@ -317,485 +360,26 @@
 
 
-/*****************************************************************************/
-/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
-/*****************************************************************************/
-
-pmCell* pmCellInFPA(
-    const psPlane* fpaCoord,
-    const pmFPA* FPA)
-{
-    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
-
-    pmChip* tmpChip = NULL;
-    psPlane chipCoord;
-    pmCell* outCell = NULL;
-
-    // Determine which chip contains the fpaCoords.
-    tmpChip = pmChipInFPA(fpaCoord, FPA);
-    if (tmpChip == NULL) {
-        return(NULL);
-    }
-
-    // Convert to those chip coordinates.
-    psPlane *rc = pmCoordFPAToChip(&chipCoord, fpaCoord, tmpChip);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine Chip coords.\n");
-        return(NULL);
-    }
-
-    // Determine which cell contains those chip coordinates.
-    outCell = pmCellInChip(&chipCoord, tmpChip);
-    if (outCell == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
-        return(NULL);
-    }
-
-    return (outCell);
-}
-
-pmChip* pmChipInFPA(
-    const psPlane* fpaCoord,
-    const pmFPA* FPA)
-{
-    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
-    PS_ASSERT_PTR_NON_NULL(FPA->chips, NULL);
-
-    psArray* chips = FPA->chips;
-    psS32 nChips = chips->n;
-    psPlane chipCoord;
-    pmCell *tmpCell = NULL;
-
-    //
-    // Loop through every chip in this FPA.  Convert the original FPA
-    // coordinates to chip coordinates for that chip.  Then, determine if any
-    // cells in that chip contain those chip coordinates.
-    // XXX: Depending on the number of chips, and their topology, there may be
-    // a much more efficient way of doing this.
-    //
-    for (psS32 i = 0; i < nChips; i++) {
-        pmChip* tmpChip = chips->data[i];
-        PS_ASSERT_PTR_NON_NULL(tmpChip, NULL);
-        PS_ASSERT_PTR_NON_NULL(tmpChip->fromFPA, NULL);
-
-        psPlaneTransformApply(&chipCoord, tmpChip->fromFPA, fpaCoord);
-
-        tmpCell = pmCellInChip(&chipCoord, tmpChip);
-        if (tmpCell != NULL) {
-            return(tmpChip);
-        }
-    }
-
-    psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the chip.\n");
-    return (NULL);
-}
-
-
-pmCell* pmCellInChip(
-    const psPlane* chipCoord,
-    const pmChip* chip)
-{
-    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(chip, NULL);
-
-    psPlane cellCoord;
-    psArray* cells;
-
-    cells = chip->cells;
-    if (cells == NULL) {
-        return NULL;
-    }
-
-    //
-    // We loop over each cell in the chip.  We transform the chipCoord into
-    // a cellCoord for that cell and determine if that cellCoord is valid.
-    // If so, then we return that cell.
-    // XXX: Depending on the number of cells, and their topology, there may be
-    // a much more efficient way of doing this.
-    //
-    for (psS32 i = 0; i < cells->n; i++) {
-        pmCell* tmpCell = (pmCell* ) cells->data[i];
-        PS_ASSERT_PTR_NON_NULL(tmpCell, NULL);
-
-        psPlaneTransform *chipToCell = NULL;
-        if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
-            chipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
-        } else {
-            psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
-            //chipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
-            chipToCell = NULL;
-        }
-        if (chipToCell == NULL) {
-            psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
-            return(NULL);
-        }
-        psArray* readouts = tmpCell->readouts;
-
-        if (readouts != NULL) {
-            for (psS32 j = 0; j < readouts->n; j++) {
-                pmReadout* tmpReadout = readouts->data[j];
-                PS_ASSERT_READOUT_NON_NULL(tmpReadout, NULL);
-
-                psPlaneTransformApply(&cellCoord,
-                                      chipToCell,
-                                      chipCoord);
-
-                if (checkValidImageCoords(cellCoord.x,
-                                          cellCoord.y,
-                                          tmpReadout->image)) {
-                    psFree(chipToCell);
-                    return (tmpCell);
-                }
-            }
-        }
-        psFree(chipToCell);
-    }
-
-    //psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
-    return (NULL);
-}
-
-
-psPlane* pmCoordCellToFPA(
-    psPlane* fpaCoord,
-    const psPlane* cellCoord,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-
-    psPlane *rc = psPlaneTransformApply(fpaCoord, cell->toFPA, cellCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to FPA coords.\n");
-    }
-    return(rc);
-}
-
-
-psPlane* pmCoordChipToFPA(
-    psPlane* outCoord,
-    const psPlane* inCoord,
-    const pmChip* chip)
-{
-    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(chip, NULL);
-
-    psPlane *rc = psPlaneTransformApply(outCoord, chip->toFPA, inCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform chip coords to FPA coords.\n");
-    }
-    return(rc);
-}
-
-
-psPlane* pmCoordFPAToChip(
-    psPlane* chipCoord,
-    const psPlane* fpaCoord,
-    const pmChip* chip)
-{
-    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(chip, NULL);
-    PS_ASSERT_PTR_NON_NULL(chip->fromFPA, NULL);
-
-    psPlane *rc = psPlaneTransformApply(chipCoord, chip->fromFPA, fpaCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to Chip coords.\n");
-    }
-    return(rc);
-}
-
-psPlane* pmCoordCellToChip(
-    psPlane* outCoord,
-    const psPlane* inCoord,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-
-    psPlane *rc = psPlaneTransformApply(outCoord, cell->toChip, inCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Cell coords to Chip coords.\n");
-    }
-    return(rc);
-}
-
-psPlane* pmCoordChipToCell(
-    psPlane* cellCoord,
-    const psPlane* chipCoord,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
-
-    pmCell *tmpCell = pmCellInChip(chipCoord, cell->parent);
-    if (tmpCell == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the proper cell.\n");
-        return(NULL);
-    }
-
-    psPlaneTransform *tmpChipToCell = NULL;
-    PS_ASSERT_PTR_NON_NULL(tmpCell->toChip, NULL);
-    if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
-        tmpChipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
-    } else {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
-        // XXX: tmpChipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
-        tmpChipToCell = NULL;
-    }
-    if (tmpChipToCell == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
-        return(NULL);
-    }
-
-    psPlane *rc = psPlaneTransformApply(cellCoord, tmpChipToCell, chipCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Chip coords to Cell coords.\n");
-    }
-    psFree(tmpChipToCell);
-    return(rc);
-}
-
-psPlane* pmCoordFPAToTP(
-    psPlane* outCoord,
-    const psPlane* inCoord,
-    double color,
-    double magnitude,
-    const pmFPA* fpa)
-{
-    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
-
-    psPlane *rc = psPlaneDistortApply(outCoord, fpa->toTangentPlane, inCoord, color, magnitude);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to tangent plane coords.\n");
-    }
-    return(rc);
-}
-
-psPlane* pmCoordTPToFPA(
-    psPlane* fpaCoord,
-    const psPlane* tpCoord,
-    double color,
-    double magnitude,
-    const pmFPA* fpa)
-{
-    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
-    PS_ASSERT_PTR_NON_NULL(fpa->fromTangentPlane, NULL);
-
-    psPlane *rc = psPlaneDistortApply(fpaCoord, fpa->fromTangentPlane, tpCoord, color, magnitude);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to FPA coords.\n");
-    }
-    return(rc);
-}
-
-
-/*****************************************************************************
-XXXDeproject(outSphere, coord, projection): This private routine is a wrapper
-for p_psDeproject().  The reason: p_psDeproject() and p_psProject() combined
-do not seem to produce the original coordinates when they even though they
-should.  XXXDeproject() simply negates the ->r and ->d members of the output
-psSphere if the input ->y is larger than 0.0.  I don't know why it works.
- 
-I'm guessing the p_psProject() and p_psDeproject() functions have bugs.
- 
-XXX: It appears that p_psProject() and p_psDeproject() have been fixed.
-Remove this.
- *****************************************************************************/
-psSphere* XXXDeproject(
-    psSphere *outSphere,
-    const psPlane* coord,
-    const psProjection* projection)
-{
-    psSphere *rc = p_psDeproject(outSphere, coord, projection);
-
-    if (coord->y >= 0.0) {
-        rc->d = -rc->d;
-        rc->r = -rc->r;
-    }
-
-    return(rc);
-}
-
-/*****************************************************************************
-  *****************************************************************************/
-psSphere* pmCoordTPToSky(
-    psSphere* outSphere,
-    const psPlane* tpCoord,
-    const psProjection *projection)
-{
-    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(projection, NULL);
-
-    //    psSphere *rc = XXXDeproject(outSphere, tpCoord, projection);
-    psSphere *rc = p_psDeproject(outSphere, tpCoord, projection);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to sky coords.\n");
-    }
-    return(rc);
-}
-
 /*****************************************************************************
  *****************************************************************************/
-psPlane* pmCoordSkyToTP(
-    psPlane* tpCoord,
-    const psSphere* in,
-    const psProjection *projection)
-{
-    PS_ASSERT_PTR_NON_NULL(in, NULL);
-    PS_ASSERT_PTR_NON_NULL(projection, NULL);
-
-    psPlane *rc = p_psProject(tpCoord, in, projection);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to tangent plane coords.\n");
-    }
-    return(rc);
-}
-
-/*****************************************************************************
- *****************************************************************************/
-psSphere* pmCoordCellToSky(
-    psSphere* skyCoord,
-    const psPlane* cellCoord,
-    double color,
-    double magnitude,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->toFPA, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->toTangentPlane, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->projection, NULL);
-    psPlane fpaCoord;
-    psPlane tpCoord;
-    psPlane *rc;
-    pmFPA* parFPA = (cell->parent)->parent;
-
-    // Convert the input cell coordinates to FPA coordinates.
-    rc = psPlaneTransformApply(&fpaCoord, cell->toFPA, cellCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to FPA coords.\n");
-        return(NULL);
-    }
-
-    // Convert the FPA coordinates to tangent plane Coordinates.
-    rc = psPlaneDistortApply(&tpCoord, parFPA->toTangentPlane, &fpaCoord, color, magnitude);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform FPA coords to tangent plane coords.\n");
-        return(NULL);
-    }
-
-    // Convert the tangent plane Coordinates to sky coordinates.
-    psSphere *rc2 = pmCoordTPToSky(skyCoord, &tpCoord, parFPA->projection);
-    if (rc2 == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to sky coords.\n");
-    }
-
-    return(rc2);
-}
-
-/*****************************************************************************
- *****************************************************************************/
-psPlane* pmCoordSkyToCell(
-    psPlane* cellCoord,
-    const psSphere* skyCoord,
-    float color,
-    float magnitude,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
-    pmChip *parChip = cell->parent;
-    pmFPA *parFPA = parChip->parent;
-    psPlane tpCoord;
-    psPlane fpaCoord;
-    psPlane chipCoord;
-    psPlane *rc;
-
-    // Convert the skyCoords to tangent plane coords.
-    rc = pmCoordSkyToTP(&tpCoord, skyCoord, parFPA->projection);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine tangent plane coords.\n");
-        return(NULL);
-    }
-
-    // Convert the tangent plane coords to FPA coords.
-    rc = pmCoordTPToFPA(&fpaCoord, &tpCoord, color, magnitude, parFPA);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine FPA coords.\n");
-        return(NULL);
-    }
-
-    // Convert the FPA coords to chip coords.
-    rc = pmCoordFPAToChip(&chipCoord, &fpaCoord, parChip);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine chip coords.\n");
-        return(NULL);
-    }
-
-    // Convert the chip coords to cell coords.
-    rc = pmCoordChipToCell(cellCoord, &chipCoord, cell);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine cell coords.\n");
-        return(NULL);
-    }
-
-    return (cellCoord);
-}
-
-/*****************************************************************************
- *****************************************************************************/
-psSphere* pmCoordCellToSkyQuick(
-    psSphere* outSphere,
-    const psPlane* cellCoord,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
-    psPlane outPlane;
-    psPlane *rc;
-    rc = psPlaneTransformApply(&outPlane, cell->toSky, cellCoord);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to sky coords.\n");
-        return(NULL);
-    }
-
-    psSphere *out = outSphere;
-    if (out == NULL) {
-        out = psSphereAlloc();
-    }
-    out->r = outPlane.y;
-    out->d = outPlane.x;
-
-    return(out);
-}
-
-/*****************************************************************************
- *****************************************************************************/
-psPlane* pmCoordSkyToCellQuick(
-    psPlane* cellCoord,
-    const psSphere* skyCoord,
-    const pmCell* cell)
-{
-    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell, NULL);
-    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
-    psPlane skyPlane;
-    skyPlane.y = skyCoord->r;
-    skyPlane.x = skyCoord->d;
-
-    psPlane *rc = psPlaneTransformApply(cellCoord, cell->toSky, &skyPlane);
-    if (rc == NULL) {
-        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to cell coords.\n");
-    }
-    return(cellCoord);
+
+// Set cells within a chip to be processed or not
+static bool setCellsProcess(const pmChip *chip, // Chip of interest
+                            bool process  // Process this chip?
+                           )
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    psArray *cells = chip->cells;       // Component cells
+    if (! cells) {
+        return false;
+    }
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *tmpCell = cells->data[i]; // Cell of interest
+        if (tmpCell) {
+            tmpCell->process = process;
+        }
+    }
+
+    return true;
 }
 
@@ -807,23 +391,26 @@
 {
     PS_ASSERT_PTR_NON_NULL(fpa, false);
-    if ((fpa->chips == NULL) || (chipNum >= fpa->chips->n)) {
+
+    psArray *chips = fpa->chips;        // Component chips
+    if ((chips == NULL) || (chipNum >= chips->n)) {
         return(false);
     }
-    psBool rc = true;
-
-    for (psS32 i = 0 ; i < fpa->chips->n ; i++) {
-        pmChip *tmpChip = (pmChip *) fpa->chips->data[i];
+
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i];
         if (tmpChip == NULL) {
-            rc = false;
+            continue;
+        }
+        if (i == chipNum) {
+            tmpChip->process = true;
+            setCellsProcess(tmpChip, true);
         } else {
-            if (i == chipNum) {
-                tmpChip->valid = true;
-            } else {
-                tmpChip->valid = false;
-            }
-        }
-    }
-
-    return(rc);
+            tmpChip->process = false;
+            setCellsProcess(tmpChip, false);
+        }
+
+    }
+
+    return true;
 }
 
@@ -831,15 +418,15 @@
 /*****************************************************************************
 XXX: The SDRS is ambiguous on a few things:
-    Whether or not the other chips should be set valid=true.
-    Should we return the number of chip valid=true before or after they're set,
+    Whether or not the other chips should be set process=true. [PAP: No]
+    Should we return the number of chip process=true before or after they're set, [PAP: After]
  *****************************************************************************/
 /**
- * 
- * pmFPAExcludeChip shall set valid to false only for the specified chip
+ *
+ * pmFPAExcludeChip shall set process to false only for the specified chip
  * number (chipNum). In the event that the specified chip number does not exist
  * within the fpa, the function shall generate a warning, and perform no action.
- * The function shall return the number of chips within the fpa that have valid
+ * The function shall return the number of chips within the fpa that have process
  * set to true.
- *  
+ *
  */
 int pmFPAExcludeChip(
@@ -849,21 +436,22 @@
     PS_ASSERT_PTR_NON_NULL(fpa, false);
 
-    if (fpa->chips == NULL) {
+    psArray *chips = fpa->chips;        // Component chips
+    if (chips == NULL) {
         psLogMsg(__func__, PS_LOG_WARN, "WARNING: fpa->chips == NULL\n");
         return(0);
     }
-    if ((chipNum >= fpa->chips->n) || (NULL == (pmChip *) fpa->chips->data[chipNum])) {
+    if ((chipNum >= chips->n) || (NULL == (pmChip *) chips->data[chipNum])) {
         psLogMsg(__func__, PS_LOG_WARN, "WARNING: the specified chip (%d) does not exist.\n", chipNum);
         return(0);
     }
 
-    psS32 numChips = 0;
-    for (psS32 i = 0 ; i < fpa->chips->n ; i++) {
-        pmChip *tmpChip = (pmChip *) fpa->chips->data[i];
+    int numChips = 0;                   // Number of chips to be processed
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i]; // Chip of interest
         if (tmpChip != NULL) {
             if (i == chipNum) {
-                tmpChip->valid = false;
-            } else {
-                tmpChip->valid = true;
+                tmpChip->process = false;
+                setCellsProcess(tmpChip, false); // Wipe out the cell as well
+            } else if (tmpChip->process) {
                 numChips++;
             }
@@ -874,2 +462,73 @@
 }
 
+
+bool pmCellSetWeights(pmCell *cell // Cell for which to set weights
+                     )
+{
+    float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // Cell gain
+    float readnoise = psMetadataLookupF32(NULL, cell->concepts, "CELL.READNOISE"); // Cell read noise
+    psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC"); // Trim section
+
+    p_pmHDU *hdu = cell->hdu;           // The data unit, containing the weight and mask originals
+    if (!hdu) {
+        pmChip *chip = cell->parent;    // The parent chip
+        if (chip->hdu) {
+            hdu = chip->hdu;
+        } else {
+            pmFPA *fpa = chip->parent;  // The parent FPA
+            hdu = fpa->hdu;
+            if (!hdu) {
+                psError(PS_ERR_UNKNOWN, true, "Unable to find the HDU in the hierarchy!\n");
+                return false;
+            }
+        }
+    }
+
+    psArray *pixels = hdu->images;      // Array of images
+    psArray *weights = hdu->weights;    // Array of weight images
+    psArray *masks = hdu->masks;        // Array of mask images
+    // Generate the weights and masks if required
+    if (! weights) {
+        weights = psArrayAlloc(pixels->n);
+        for (int i = 0; i < pixels->n; i++) {
+            psImage *image = pixels->data[i];
+            weights->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+            psImageInit(weights->data[i], 0.0);
+        }
+        hdu->weights = weights;
+    }
+    if (! masks) {
+        masks = psArrayAlloc(pixels->n);
+        for (int i = 0; i < pixels->n; i++) {
+            psImage *image = pixels->data[i];
+            masks->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+            psImageInit(masks->data[i], 0);
+        }
+        hdu->masks = masks;
+    }
+
+    // Set the pixels
+    psArray *readouts = cell->readouts; // Array of readouts
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout of interest
+
+        if (! readout->weight) {
+            readout->weight = weights->data[i];
+        }
+        if (! readout->mask) {
+            readout->mask = masks->data[i];
+        }
+
+        // Mask is already set to 0
+
+        // Set weight image to the variance = g*f + rn^2
+        psImage *image = psImageSubset(readout->image, *trimsec); // The pixels
+        psImage *weight = psImageSubset(readout->weight, *trimsec); // The weight map
+        psBinaryOp(weight, image, "*", psScalarAlloc(gain, PS_TYPE_F32));
+        psBinaryOp(weight, weight, "+", psScalarAlloc(readnoise*readnoise, PS_TYPE_F32));
+    }
+
+    return true;
+}
+
+
Index: trunk/psModules/src/astrom/pmAstrometry.h
===================================================================
--- trunk/psModules/src/astrom/pmAstrometry.h	(revision 6529)
+++ trunk/psModules/src/astrom/pmAstrometry.h	(revision 6872)
@@ -8,6 +8,6 @@
 *  @author GLG, MHPCC
 *
-*  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
-*  @date $Date: 2006-01-26 21:10:50 $
+*  @version $Revision: 1.7 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
@@ -30,14 +30,13 @@
 {
     const char *extname;                // Extension name, if it corresponds to this level
-    psArray *pixels;                    // The pixel data, 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;
 
-p_pmHDU *pmHDUAlloc();
-void *pmHDUFree(p_pmHDU *hdu);
-
 /** Focal plane data structure
- * 
+ *
  *  A focal plane consists of one or more chips (according to the number of
  *  pieces of contiguous silicon). It contains metadata containers for the
@@ -52,5 +51,5 @@
  *  transformation which will be derived from numerically inverting the forward
  *  transformation.
- *  
+ *
  */
 typedef struct
@@ -71,5 +70,5 @@
 
 /** Chip data structure
- *  
+ *
  *  A chip consists of one or more cells (according to the number of amplifiers
  *  on the device). The chip contains metadata containers for the concepts and
@@ -82,5 +81,5 @@
  *  inverting the forward transformation. A boolean indicates whether the chip is
  *  of interest, allowing it to be excluded from analysis.
- *  
+ *
  */
 typedef struct
@@ -97,5 +96,6 @@
     psArray *cells;                     ///< The cells (referred to by name)
     pmFPA *parent;                      ///< Parent FPA
-    bool valid;                         ///< Do we bother about reading and working with this chip?
+    bool process;                       ///< Do we bother about reading and working with this chip?
+    bool exists;                        ///< Does the chip exist --- has it been read in?
     p_pmHDU *hdu;                       ///< FITS data
 }
@@ -126,5 +126,6 @@
     psArray *readouts;                  ///< The readouts (referred to by number)
     pmChip *parent;                     ///< Parent chip
-    bool valid;                         ///< Do we bother about reading and working with this cell?
+    bool process;                       ///< Do we bother about reading and working with this cell?
+    bool exists;                        ///< Does the cell exist --- has it been read in?
     p_pmHDU *hdu;                       ///< FITS data
 }
@@ -144,4 +145,5 @@
 {
     // Position on the cell
+    // XXX: These may be removed in the future; use parent->concepts instead?
     int col0;                           ///< Offset from the left of chip.
     int row0;                           ///< Offset from the bottom of chip.
@@ -152,5 +154,4 @@
     psImage *mask;                      ///< Mask of input image
     psImage *weight;                    ///< Weight of input image
-    psList *bias;                       ///< List of bias section (sub-)images
     psMetadata *analysis;               ///< Readout-level analysis metadata
     pmCell *parent;                     ///< Parent cell
@@ -183,11 +184,11 @@
  */
 pmCell *pmCellAlloc(
-    pmChip *chip,                        ///< Parent chip
-    psMetadata *cameradata,
-    psString name
+    pmChip *chip,       ///< Parent chip
+    psMetadata *cameraData, ///< Camera data
+    const char *name    ///< Name of cell
 );
 
 /** Allocates a pmChip
- * 
+ *
  *  The constructor shall make an empty pmChip. If the parent fpa is not NULL,
  *  the parent link is made and the chip shall be placed in the parent's array
@@ -199,16 +200,15 @@
  */
 pmChip *pmChipAlloc(
-    pmFPA *fpa,
-    psString name
-
+    pmFPA *fpa,                         ///< FPA to which the chip belongs
+    const char *name                    ///< Name of chip
 );
 
 /** Allocates a pmFPA
- * 
+ *
  *  The constructor shall make an empty pmFPA. The chips array shall be
  *  allocated with a zero size, the camera and db pointers set to the values
  *  provided, and the concepts metadata constructed. All other pointers in the
  *  structure shall be initialized to NULL.
- * 
+ *
  */
 pmFPA *pmFPAAlloc(
@@ -216,12 +216,21 @@
 );
 
+/** Allocates a p_pmHDU
+ *
+ * XXX: More detailed description
+ *
+ * @return p_pmHDU*    newly allocated p_pmHDU
+ */
+p_pmHDU *p_pmHDUAlloc(const char *extname // Extension name
+                     );
+
 
 /** Verify parent links.
- * 
+ *
  *  This function checks the validity of the parent links in the FPA hierarchy.
  *  If a parent link is not set (or not set correctly), it is corrected, and the
  *  function shall return false. If all the parent pointers were correct, the
  *  function shall return true.
- * 
+ *
  */
 bool pmFPACheckParents(
@@ -232,10 +241,38 @@
 
 /** FUNC DESC
- * 
- * 
- * 
- * 
- */
-
+ *
+ *
+ *
+ *
+ */
+
+
+
+/**
+ *
+ * pmFPASelectChip shall set valid to true for the specified chip number
+ * (chipNum), and all other chips shall have valid set to false. In the event
+ * that the specified chip number does not exist within the fpa, the function
+ * shall return false.
+ *
+ */
+bool pmFPASelectChip(
+    pmFPA *fpa,
+    int chipNum
+);
+
+/**
+ *
+ * pmFPAExcludeChip shall set valid to false only for the specified chip
+ * number (chipNum). In the event that the specified chip number does not exist
+ * within the fpa, the function shall generate a warning, and perform no action.
+ * The function shall return the number of chips within the fpa that have valid
+ * set to true.
+ *
+ */
+int pmFPAExcludeChip(
+    pmFPA *fpa,
+    int chipNum
+);
 
 
@@ -426,47 +463,4 @@
 );
 
-/**
- * 
- * pmFPASelectChip shall set valid to true for the specified chip number
- * (chipNum), and all other chips shall have valid set to false. In the event
- * that the specified chip number does not exist within the fpa, the function
- * shall return false.
- *  
- */
-bool pmFPASelectChip(
-    pmFPA *fpa,
-    int chipNum
-);
-
-
-/**
- * 
- * pmFPAExcludeChip shall set valid to false only for the specified chip
- * number (chipNum). In the event that the specified chip number does not exist
- * within the fpa, the function shall generate a warning, and perform no action.
- * The function shall return the number of chips within the fpa that have valid
- * set to true.
- *  
- */
-int pmFPAExcludeChip(
-    pmFPA *fpa,
-    int chipNum
-);
-
-
-/**
- * 
- * pmFPAConstruct shall construct a focal plane hierarchy from a camera
- * configuration. A db handle is also provided so that may be set in the pmFPA.
- * The resultant pmFPA and its lower-down components shall be ready for to read a
- * FITS file into it by setting the extname pointers at the appropriate levels to
- * the appropriate FITS extension name.
- *  
- */
-pmFPA *pmFPAConstruct(
-    const psMetadata *camera,
-    psDB *db
-);
-
 
 #endif // #ifndef PS_ASTROMETRY_H
Index: trunk/psModules/src/astrom/pmAstrometryObjects.c
===================================================================
--- trunk/psModules/src/astrom/pmAstrometryObjects.c	(revision 6529)
+++ trunk/psModules/src/astrom/pmAstrometryObjects.c	(revision 6872)
@@ -8,6 +8,6 @@
 *  @author EAM, IfA
 *
-*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
-*  @date $Date: 2006-03-04 01:01:33 $
+*  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
@@ -20,5 +20,5 @@
 #include <math.h>
 #include "pslib.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 #include "pmAstrometryObjects.h"
 
@@ -540,5 +540,6 @@
     psArray *rot;
 
-    pmAstromStats minStat, newStat;
+    pmAstromStats minStat;
+    pmAstromStats newStat = {{0, 0, 0, 0}, {0, 0, 0, 0}, 0, 0, 0, 0};
     psPlane center;
 
Index: trunk/psModules/src/astrom/pmAstrometryObjects.h
===================================================================
--- trunk/psModules/src/astrom/pmAstrometryObjects.h	(revision 6529)
+++ trunk/psModules/src/astrom/pmAstrometryObjects.h	(revision 6872)
@@ -8,6 +8,6 @@
 *  @author EAM, IfA
 *
-*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
-*  @date $Date: 2006-01-26 21:10:50 $
+*  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
@@ -21,8 +21,8 @@
 # include <unistd.h>   // for unlink
 # include <pslib.h>
-# include <pmAstrometry.h>
-
-/*
- * 
+# include <pmFPA.h>
+
+/*
+ *
  * This structure specifies the coordinate of the detection in each of the
  * four necessary coordinate frames: pix defines the position in the psReadout
@@ -35,7 +35,7 @@
  * coordinates, while the reference detections will be projected to the other
  * frames from the sky coordinates.
- * 
+ *
  * XXX: There are more members here than in the SDRS.
- * 
+ *
  */
 typedef struct
@@ -55,9 +55,9 @@
 
 /*
- * 
+ *
  * The pmAstromMatch structure defines the cross-correlation between two
  * arrays. A single such data item specifies that item number pmAstromMatch.idx1
  * in the first list corresponds to pmAstromMatch.idx2 in the second list.
- * 
+ *
  */
 typedef struct
@@ -70,7 +70,7 @@
 
 /*
- * 
+ *
  * XXX: Not in SDRS.
- * 
+ *
  */
 typedef struct
@@ -88,5 +88,5 @@
 
 /*
- * 
+ *
  * If the two sets of coordinates are expected to agree very well (ie, the
  * current best-guess astrometric solution is quite close to the radius. The
@@ -97,5 +97,5 @@
  * ASTROM.MATCH.RADIUS). The output consists an array of pmAstromMatch values,
  * defined below.
- * 
+ *
  */
 psArray *pmAstromRadiusMatch(
@@ -108,11 +108,11 @@
 
 /*
- * 
+ *
  * This function accepts an array of pmAstromObj objects and rotates them by
  * the given angle about the given center coordinate pCenter,qCenter in the Focal
  * Plane Array coordinates.
- * 
+ *
  * XXX: This differs from the SDRS
- * 
+ *
  */
 /* SDRS
@@ -132,5 +132,5 @@
 
 /*
- * 
+ *
  * If the two sets of coordinates are not known to agree well, but the
  * relative scale and approximate relative rotation is known, then a much faster
@@ -147,7 +147,7 @@
  * allowing the procedure to scan over a range of rotations. We define the
  * following function to apply this matching algorithm:
- * 
+ *
  * XXX: In the SDRS, this function is a pointer.
- * 
+ *
  */
 pmAstromStats pmAstromGridMatch(
@@ -159,5 +159,5 @@
 
 /*
- * 
+ *
  * The result of a pmAstromGridMatch may be used to modify the astrometry
  * transformation information for a pmFPA image hierarchy structure. The result
@@ -167,7 +167,7 @@
  * the linear terms of the pmFPA.toTangentPlane transformation. These two
  * adjustments are made using the function:
- * 
+ *
  * XXX: This function name is different in the SDRS.
- * 
+ *
  */
 psPlaneTransform *pmAstromGridApply(
@@ -178,9 +178,11 @@
 
 /*
- * 
+ *
  * This function is identical to pmAstromGridMatch, but is valid for only a
  * single relative rotation. The input config information need not contain any of
  * the GRID.*.ANGLE entries (they will be ignored).
- * 
+ *
+ * XXX: This function name is different in the SDRS.
+ *
  */
 /* in pmAstromGrid.c */
@@ -193,5 +195,5 @@
 
 /*
- * 
+ *
  * This function accepts the raw and reference source lists and the list of
  * matched entries. It uses the matched list to determine a polynomial
@@ -207,7 +209,7 @@
  * modifications to pmFPA.toTangentPlane incorporate the rotation component of
  * the linear terms and the higher-order terms of the polynomial fits.
- * 
+ *
  * XXX: No prototype code.
- * 
+ *
  */
 bool pmAstromFitFPA(
@@ -233,7 +235,7 @@
  *ASTROM.ORDER).  The result of this fit is a set of modifications of the
  *components of the pmChip.toFPA transformation.
- * 
+ *
  * XXX: No prototype code.
- * 
+ *
  */
 bool pmAstromFitChip(
@@ -247,5 +249,5 @@
 
 /*
- * 
+ *
  * The following function determines the position residual, in the tangent
  * plane, as a function of position in the focal plane, for a collection of raw
@@ -253,7 +255,7 @@
  * the bin size over which the gradient is measured (keyword: ASTROM.GRAD.BOX).
  * The function returns an array of pmAstromGradient structures, defined below.
- * 
+ *
  * XXX: No prototype code.
- * 
+ *
  */
 psArray pmAstromMeasureGradients(
@@ -267,9 +269,9 @@
 
 /*
- * 
+ *
  * The following data structure carries the information about the residual
  * gradient of source positions in the tangent plane (pmAstromObj.TP) as a
  * function of position in the focal plane (pmAstromObj.FP).
- * 
+ *
  */
 typedef struct
@@ -283,5 +285,5 @@
 
 /*
- * 
+ *
  * The gradient set measured above can be fitted with a pair of 2D
  * polynomials. The resulting fits can then be related back to the implied
@@ -290,7 +292,7 @@
  * supplied pmFPA structure. The configuration variable supplies the polynomial
  * order (keyword: ASTROM.DISTORT.ORDER).
- * 
+ *
  * XXX: No prototype code.
- * 
+ *
  */
 psArray pmAstromFitDistortion(
@@ -308,15 +310,15 @@
  ******************************************************************************/
 /*
- * 
- * 
- * 
- * 
- */
-
-
-/*
- * 
+ *
+ *
+ *
+ *
+ */
+
+
+/*
+ *
  * Allocates a pmAstromObj struct.
- * 
+ *
  */
 pmAstromObj *pmAstromObjAlloc (void);
@@ -325,7 +327,7 @@
 
 /*
- * 
+ *
  * Copies a pmAstromObj struct.
- * 
+ *
  */
 pmAstromObj *pmAstromObjCopy(
@@ -336,7 +338,7 @@
 
 /*
- * 
- * 
- * 
+ *
+ *
+ *
  */
 pmAstromMatch *pmAstromMatchAlloc(
@@ -349,7 +351,7 @@
 
 /*
- * 
- * 
- * 
+ *
+ *
+ *
  */
 psPlaneTransform *pmAstromMatchFit(
@@ -364,7 +366,7 @@
 
 /*
- * 
- * 
- * 
+ *
+ *
+ *
  */
 int pmAstromObjSortByFPX(
@@ -376,7 +378,7 @@
 
 /*
- * 
- * 
- * 
+ *
+ *
+ *
  */
 int pmAstromObjSortByMag(
Index: trunk/psModules/src/astrom/pmChipMosaic.c
===================================================================
--- trunk/psModules/src/astrom/pmChipMosaic.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmChipMosaic.c	(revision 6872)
@@ -0,0 +1,367 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmChipMosaic.h"
+
+#define MEM_LEAKS 0
+
+// 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 = - INT_MAX;
+    int yMin = INT_MAX;
+    int yMax = - INT_MAX;
+    psElemType type = 0;
+    for (int i = 0; i < source->n; i++) {
+        psImage *image = source->data[i]; // The image of interest
+        if (!image) {
+            continue;
+        }
+
+        // Only implemented for F32 and U8 images so far.
+        assert(image->type.type == PS_TYPE_F32 || image->type.type == PS_TYPE_U8);
+        // All input types must be the same
+        if (type == 0) {
+            type = image->type.type;
+        }
+        assert(type == image->type.type);
+
+        // 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, type); // 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 (!image) {
+            continue;
+        }
+        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.
+                    #define FILL_IN(TYPE)                                                                    \
+                    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.TYPE[yTarget][xTarget] += image->data.TYPE[y][x];                   \
+                        }                                                                                    \
+                    }
+
+                    switch (type) {
+                    case PS_TYPE_F32:
+                        FILL_IN(F32);
+                        break;
+                    case PS_TYPE_U8:
+                        FILL_IN(U8);
+                        break;
+                    default:
+                        psAbort(__func__, "Should never get here.\n");
+                    }
+
+                }
+            } // Iterating over input image
+        }
+    }
+
+    return mosaic;
+}
+
+
+// Set the concepts in the new cell, based on the values in the old one
+static bool cellConcepts(pmCell *target,// Target cell
+                         psArray *sources, // Source cells
+                         int xBin, int yBin // Binning
+                        )
+{
+    bool success = true;                // Result of setting everything
+    float gain       = 0.0;             // Gain
+    float readnoise  = 0.0;             // Read noise
+    float saturation = 0.0;             // Saturation level
+    float bad        = 0.0;             // Bad level
+    float exposure   = 0.0;             // Exposure time
+    float darktime   = 0.0;             // Dark time
+    double time      = 0.0;             // Time of observation
+    psTimeType timeSys = 0;             // Time system
+
+    int nCells = 0;                     // Number of cells;
+    for (int i = 0; i < sources->n; i++) {
+        pmCell *cell = sources->data[i];// The cell of interest
+        if (!cell) {
+            continue;
+        }
+
+        nCells++;
+        gain       += psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN");
+        readnoise  += psMetadataLookupF32(NULL, cell->concepts, "CELL.READNOISE");
+        saturation += psMetadataLookupF32(NULL, cell->concepts, "CELL.SATURATION");
+        bad        += psMetadataLookupF32(NULL, cell->concepts, "CELL.BAD");
+        exposure   += psMetadataLookupF32(NULL, cell->concepts, "CELL.EXPOSURE");
+        darktime   += psMetadataLookupF32(NULL, cell->concepts, "CELL.DARKTIME");
+        time       += psTimeToMJD(psMetadataLookupPtr(NULL, cell->concepts, "CELL.TIME"));
+        if (i == 0) {
+            timeSys = psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS");
+        } else if (timeSys != psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS")) {
+            psLogMsg(__func__, PS_LOG_ERROR, "Differing time systems in use: %d vs %d\n", timeSys,
+                     psMetadataLookupS32(NULL, cell->concepts, "CELL.TIMESYS"));
+            success = false;
+        }
+    }
+    gain       /= (float)nCells;
+    readnoise  /= (float)nCells;
+    saturation /= (float)nCells;
+    bad        /= (float)nCells;
+    exposure   /= (float)nCells;
+    darktime   /= (float)nCells;
+    psTime *timePtr = psTimeFromMJD(time/(double)nCells);
+    timePtr = psTimeConvert(timePtr, timeSys);
+
+    // XXX *REALLY* need a generic "concept update" function that handles the type and comments transparently.
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.GAIN", PS_META_REPLACE, "Gain (e/ADU)", gain);
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.READNOISE", PS_META_REPLACE, "Read noise (e)", readnoise);
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.SATURATION", PS_META_REPLACE, "Saturation level (ADU)", saturation);
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.BAD", PS_META_REPLACE, "Bad level (ADU)", bad);
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.EXPOSURE", PS_META_REPLACE, "Exposure time (sec)", exposure);
+    psMetadataAddF32(target->concepts, PS_LIST_TAIL, "CELL.DARKTIME", PS_META_REPLACE, "Time since last CCD flush (sec)", darktime);
+    psMetadataAddPtr(target->concepts, PS_LIST_TAIL, "CELL.TIME", PS_DATA_TIME | PS_META_REPLACE, "Time of observation", timePtr);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_META_REPLACE, "Time system", timeSys);
+    psFree(timePtr);
+
+    // Now fill in the ones I know by other means
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.X0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.Y0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", 1);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", 1);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "Binning in x", xBin);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "Binning in x", yBin);
+    psMetadataAddS32(target->concepts, PS_LIST_TAIL, "CELL.READDIR", PS_META_REPLACE, "Read direction (faked)", 1);
+    psRegion *trimsec = psMetadataLookupPtr(NULL, target->concepts, "CELL.TRIMSEC");
+    trimsec->x0 = trimsec->x1 = trimsec->y0 = trimsec->y1 = 0.0;
+
+    return success;
+}
+
+
+
+// Mosaic a chip together into a single image
+int 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
+    psArray *weights = psArrayAlloc(cells->n); // Array of weight images to be mosaicked
+    psArray *masks = psArrayAlloc(cells->n); // Array of mask images to 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
+    psTrace(__func__, 1, "Mosaicking %d cells...\n", cells->n);
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        if (!cell) {
+            continue;
+        }
+        x0->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.X0");
+        y0->data.S32[i] = psMetadataLookupS32(NULL, cell->concepts, "CELL.Y0");
+        psTrace(__func__, 5, "Cell %d: x0=%d y0=%d\n", i, x0->data.S32[i], y0->data.S32[i]);
+        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;
+        }
+
+        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);
+        }
+        pmReadout *readout = readouts->data[0]; // The only readout we'll bother with
+
+        // The images to put into the mosaic
+        images->data[i]  = psMemIncrRefCounter(readout->image);
+        weights->data[i] = psMemIncrRefCounter(readout->weight);
+        masks->data[i]   = psMemIncrRefCounter(readout->mask);
+    }
+    // Mosaic the images together and we're done
+    psImage *image = p_pmImageMosaic(images, xFlip, yFlip, xBin, yBin, xBinChip, yBinChip, x0, y0);
+    psImage *weight = p_pmImageMosaic(weights, xFlip, yFlip, xBin, yBin, xBinChip, yBinChip, x0, y0);
+    psImage *mask = p_pmImageMosaic(masks, xFlip, yFlip, xBin, yBin, xBinChip, yBinChip, x0, y0);
+
+    // Clean up
+    psFree(x0);
+    psFree(y0);
+    psFree(xBin);
+    psFree(yBin);
+    psFree(xFlip);
+    psFree(yFlip);
+    psFree(images);
+    psFree(weights);
+    psFree(masks);
+    int nCells = cells->n;
+
+    // Fix up the HDU
+    if (chip->parent->hdu) {
+        psLogMsg(__func__, PS_LOG_WARN, "The original format has the entire FPA in a single extension.  "
+                 "The FPA hierarchy may be invalid following the pmChipMosaic.\n");
+    } else {
+        if (! chip->hdu) {
+            psString chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+            chip->hdu = p_pmHDUAlloc(chipName);
+        }
+        p_pmHDU *hdu = chip->hdu;
+        psArrayElementsFree(hdu->images);
+        psArrayElementsFree(hdu->weights);
+        psArrayElementsFree(hdu->masks);
+        hdu->images  = psArrayRealloc(hdu->images,1);
+        hdu->weights = psArrayRealloc(hdu->weights, 1);
+        hdu->masks   = psArrayRealloc(hdu->masks, 1);
+        hdu->images->data[0]  = image;
+        hdu->weights->data[0] = weight;
+        hdu->masks->data[0]   = mask;
+        psMetadataAddS32(hdu->header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Number of columns", image->numCols);
+        psMetadataAddS32(hdu->header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Number of rows", image->numRows);
+    }
+
+    // Chop off all the component cells, and construct a new one
+    pmCell *newCell = pmCellAlloc(NULL, NULL, __func__); // New cell
+    cellConcepts(newCell, cells, xBinChip, yBinChip);
+    pmChipFreeCells(chip);
+    // Have to put in the new cell manually, since we didn't want to put it in before blowing the cells away.
+    newCell->parent = chip;
+    psArrayAdd(chip->cells, 1, newCell);
+    newCell->exists = true;
+    newCell->process = true;
+
+    // Now make a new readout to go in the new cell
+    pmReadout *newReadout = pmReadoutAlloc(newCell); // New readout
+    // Want the readouts to contain a subimage, but that subimage is the whole image.
+    // This preserves the relationship there was before, where freeing the parent frees the child.
+    psRegion entire = {0.0, 0.0, 0.0, 0.0};
+    newReadout->image = psMemIncrRefCounter(psImageSubset(image, entire));
+    newReadout->weight = psMemIncrRefCounter(psImageSubset(weight, entire));
+    newReadout->mask = psMemIncrRefCounter(psImageSubset(mask, entire));
+    // Drop references
+    psFree(newReadout);
+    psFree(newCell);
+
+    // Well, we've stuffed around with the camera configuration, so it's no longer valid...
+    #if 0
+
+    psFree(chip->parent->camera);
+    chip->parent->camera = NULL;
+    #endif
+
+    return nCells;
+}
+
+
+int pmFPAMosaicCells(pmFPA *fpa,        // FPA
+                     int xBinChip, int yBinChip // Binning of mosaic image in x and y
+                    )
+{
+    assert(fpa);
+
+    int numChips = 0;
+    psArray *chips = fpa->chips;        // Component chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The chip of interest
+        if (! chip || ! chip->exists || ! chip->process) {
+            continue;
+        }
+
+        if (pmChipMosaic(chip, xBinChip, yBinChip) > 0) {
+            numChips++;
+        }
+    }
+
+    return numChips;
+
+}
Index: trunk/psModules/src/astrom/pmChipMosaic.h
===================================================================
--- trunk/psModules/src/astrom/pmChipMosaic.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmChipMosaic.h	(revision 6872)
@@ -0,0 +1,26 @@
+#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 a chip together into a single cell with single readout
+int pmChipMosaic(pmChip *chip,// Chip to mosaic
+                 int xBinChip, int yBinChip // Binning of mosaic image in x and y
+                );
+
+// Mosaic all the cells in all (valid) chips together (neglecting the overscans); return number of chips
+int pmFPAMosaicCells(pmFPA *fpa,        // FPA
+                     int xBinChip, int yBinChip // Binning of mosaic image in x and y
+                    );
+
+#endif
Index: trunk/psModules/src/astrom/pmConcepts.c
===================================================================
--- trunk/psModules/src/astrom/pmConcepts.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmConcepts.c	(revision 6872)
@@ -0,0 +1,563 @@
+// XXX *REALLY* need generic "concept update" and "concept read" functions that handles the type transparently
+
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+#include "pmConceptsStandard.h"
+#include "psAdditionals.h"
+
+static bool conceptsInitialised = false;// Have concepts been read?
+static psMetadata *conceptsFPA = NULL;  // Known concepts for FPA
+static psMetadata *conceptsChip = NULL; // Known concepts for chip
+static psMetadata *conceptsCell = NULL; // Known concepts for cell
+
+// Free a concept
+static void conceptSpecFree(pmConceptSpec *spec)
+{
+    psFree(spec->blank);
+}
+
+pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, // Blank value; contains the name
+                                  pmConceptParseFunc parse, // Function to call to parse the concept
+                                  pmConceptFormatFunc format // Function to call to format the concept
+                                 )
+{
+    pmConceptSpec *spec = psAlloc(sizeof(pmConceptSpec));
+    psMemSetDeallocator(spec, (psFreeFunc)conceptSpecFree);
+
+    spec->blank = psMemIncrRefCounter(blank);
+    spec->parse = parse;
+    spec->format = format;
+
+    return spec;
+}
+
+
+bool pmConceptRegister(psMetadataItem *blank, // Blank value; contains the name
+                       pmConceptParseFunc parse, // Function to call to parse the concept
+                       pmConceptFormatFunc format, // Function to call to format the concept
+                       pmConceptLevel level // Level at which to store concept in the FPA hierarchy
+                      )
+{
+    assert(blank);
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    pmConceptSpec *spec = pmConceptSpecAlloc(blank, parse, format); // The concept specification
+    psMetadata **target = NULL;         // The metadata of known concepts to write to
+    switch (level) {
+    case PM_CONCEPT_LEVEL_FPA:
+        target = &conceptsFPA;
+        break;
+    case PM_CONCEPT_LEVEL_CHIP:
+        target = &conceptsChip;
+        break;
+    case PM_CONCEPT_LEVEL_CELL:
+        target = &conceptsCell;
+        break;
+    default:
+        psError(PS_ERR_IO, true, "Unknown concept level provided: %d\n", level);
+        psFree(spec);
+        return false;
+    }
+
+    psMetadataAdd(*target, PS_LIST_TAIL, blank->name, PS_DATA_UNKNOWN | PS_META_REPLACE,
+                  "Concepts specification", spec);
+    psFree(spec);                       // Drop reference
+
+    return true;
+}
+
+
+// Set all registered concepts to blank value for the specified level
+static bool conceptsBlank(psMetadata **specs, // One of the concepts specifications
+                          psMetadata *target // Place to install the concepts
+                         )
+{
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+    psMetadataIterator *specsIter = psMetadataIteratorAlloc(*specs, PS_LIST_HEAD, NULL); // Iterator on specs
+    psMetadataItem *specItem = NULL;    // Item from the specs metadata
+    while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+        psTrace(__func__, 9, "Blanking %s...\n", specItem->name);
+        pmConceptSpec *spec = specItem->data.V; // The specification
+        psMetadataItem *blank = spec->blank; // The concept
+        psMetadataItem *copy = NULL;    // Copy of the blank concept
+        // Trap the lists, which can't be copied in the ordinary way without a warning
+        if (blank->type == PS_DATA_LIST) {
+            copy = psMetadataItemAllocPtr(blank->name, PS_DATA_LIST, blank->comment, blank->data.V);
+        } else {
+            copy = psMetadataItemCopy(blank);
+        }
+        if (!psMetadataAddItem(target, copy, PS_LIST_TAIL, PS_META_REPLACE)) {
+            psLogMsg(__func__, PS_LOG_WARN, "Unable to add blank version of concept %s\n", blank->name);
+        }
+        psFree(copy);                   // Drop reference
+    }
+    psFree(specsIter);
+
+    return true;
+}
+
+
+
+// Read all registered concepts for the specified level
+static bool conceptsRead(psMetadata **specs, // One of the concepts specifications
+                         pmFPA *fpa,    // The FPA
+                         pmChip *chip,  // The chip
+                         pmCell *cell,  // The cell
+                         pmConceptSource source, // The source of the concepts to read
+                         psDB *db,      // Database handle
+                         psMetadata *target // Place into which to read the concepts
+                        )
+{
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    if (source & PM_CONCEPT_SOURCE_CAMERA || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsReadFromCamera(*specs, cell, target);
+    }
+    if (source & PM_CONCEPT_SOURCE_DEFAULTS || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsReadFromDefaults(*specs, fpa, chip, cell, target);
+    }
+    if (source & PM_CONCEPT_SOURCE_HEADER || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsReadFromHeader(*specs, fpa, chip, cell, target);
+    }
+    if (source & PM_CONCEPT_SOURCE_DATABASE || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsReadFromDatabase(*specs, fpa, chip, cell, db, target);
+    }
+
+    return true;
+}
+
+// Write all registered concepts for the specified level
+static bool conceptsWrite(psMetadata **specs, // One of the concepts specifications
+                          pmFPA *fpa,   // The FPA
+                          pmChip *chip, // The chip
+                          pmCell *cell, // The cell
+                          pmConceptSource source, // The source of the concepts to write
+                          psDB *db,      // Database handle
+                          psMetadata *concepts // The concepts to write out
+                         )
+{
+    if (!conceptsInitialised) {
+        pmConceptsInit();
+    }
+
+    if (source & PM_CONCEPT_SOURCE_CAMERA || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsWriteToCamera(*specs, cell, concepts);
+    }
+    if (source & PM_CONCEPT_SOURCE_DEFAULTS || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsWriteToDefaults(*specs, fpa, chip, cell, concepts);
+    }
+    if (source & PM_CONCEPT_SOURCE_HEADER || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsWriteToHeader(*specs, fpa, chip, cell, concepts);
+    }
+    if (source & PM_CONCEPT_SOURCE_DATABASE || source == PM_CONCEPT_SOURCE_ALL) {
+        pmConceptsWriteToDatabase(*specs, fpa, chip, cell, db, concepts);
+    }
+
+    return true;
+}
+
+// Set the concepts for a given FPA to blanks
+bool pmConceptsBlankFPA(pmFPA *fpa    // FPA for which to set blank concepts
+                       )
+{
+    psTrace("psModule.concepts", 5, "Blanking FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
+    return conceptsBlank(&conceptsFPA, fpa->concepts);
+}
+
+
+// Read the concepts for a given FPA
+bool pmConceptsReadFPA(pmFPA *fpa,      // FPA for which to read concepts
+                       pmConceptSource source, // The source of the concepts to read
+                       psDB *db         // Database handle
+                      )
+{
+    psTrace("psModule.concepts", 5, "Reading FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
+    return conceptsRead(&conceptsFPA, fpa, NULL, NULL, source, db, fpa->concepts);
+}
+
+// Read the concepts for a given FPA
+bool pmConceptsWriteFPA(pmFPA *fpa,     // FPA for which to write concepts
+                        pmConceptSource source, // The source of the concepts to read
+                        psDB *db        // Database handle
+                       )
+{
+    psTrace("psModule.concepts", 5, "Writing FPA concepts: %x %x\n", conceptsFPA, fpa->concepts);
+    return conceptsWrite(&conceptsFPA, fpa, NULL, NULL, source, db, fpa->concepts);
+}
+
+// Set the concepts for a given chip to blanks
+bool pmConceptsBlankChip(pmChip *chip // FPA for which to set blank concepts
+                        )
+{
+    psTrace("psModule.concepts", 5, "Blanking chip concepts: %x %x\n", conceptsChip, chip->concepts);
+    return conceptsBlank(&conceptsChip, chip->concepts);
+}
+
+// Read the concepts for a given FPA
+bool pmConceptsReadChip(pmChip *chip,   // Chip for which to read concepts
+                        pmConceptSource source, // The source of the concepts to read
+                        bool propagate, // Propagate to higher levels as well?
+                        psDB *db        // Database handle
+                       )
+{
+    psTrace("psModule.concepts", 5, "Reading chip concepts: %x %x\n", conceptsChip, chip->concepts);
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    return conceptsRead(&conceptsChip, fpa, chip, NULL, source, db, chip->concepts) &&
+           ((propagate && conceptsRead(&conceptsFPA, fpa, chip, NULL, source, db, fpa->concepts)) ||
+            !propagate);
+}
+
+// Read the concepts for a given FPA
+bool pmConceptsWriteChip(pmChip *chip,  // Chip for which to write concepts
+                         pmConceptSource source, // The source of the concepts to read
+                         bool propagate,// Propagate to higher levels as well?
+                         psDB *db        // Database handle
+                        )
+{
+    psTrace("psModule.concepts", 5, "Writing chip concepts: %x %x\n", conceptsChip, chip->concepts);
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    return conceptsWrite(&conceptsChip, fpa, chip, NULL, source, db, chip->concepts) &&
+           ((propagate && conceptsWrite(&conceptsFPA, fpa, chip, NULL, source, db, fpa->concepts)) ||
+            !propagate);
+}
+
+// Set the concepts for a given chip to blanks
+bool pmConceptsBlankCell(pmCell *cell // Cell for which to set blank concepts
+                        )
+{
+    psTrace("psModule.concepts", 5, "Blanking cell concepts: %x %x\n", conceptsCell, cell->concepts);
+    return conceptsBlank(&conceptsCell, cell->concepts);
+}
+
+// Read the concepts for a given FPA
+bool pmConceptsReadCell(pmCell *cell,   // Cell for which to read concepts
+                        pmConceptSource source, // The source of the concepts to read
+                        bool propagate,// Propagate to higher levels as well?
+                        psDB *db        // Database handle
+                       )
+{
+    psTrace("psModule.concepts", 5, "Reading cell concepts: %x %x\n", conceptsCell, cell->concepts);
+    pmChip *chip = cell->parent;        // Chip to which the cell belongs
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    return conceptsRead(&conceptsCell, fpa, chip, cell, source, db, cell->concepts) &&
+           ((propagate && conceptsRead(&conceptsChip, fpa, chip, cell, source, db, chip->concepts) &&
+             conceptsRead(&conceptsFPA, fpa, chip, cell, source, db, fpa->concepts)) || !propagate);
+}
+
+// Read the concepts for a given FPA
+bool pmConceptsWriteCell(pmCell *cell,  // FPA for which to write concepts
+                         pmConceptSource source, // The source of the concepts to read
+                         bool propagate,// Propagate to higher levels as well?
+                         psDB *db       // Database handle
+                        )
+{
+    psTrace("psModule.concepts", 5, "Writing cell concepts: %x %x\n", conceptsCell, cell->concepts);
+    pmChip *chip = cell->parent;        // Chip to which the cell belongs
+    pmFPA *fpa = chip->parent;          // FPA to which the chip belongs
+    return conceptsWrite(&conceptsCell, fpa, chip, cell, source, db, cell->concepts) &&
+           ((propagate && conceptsWrite(&conceptsChip, fpa, chip, cell, source, db, chip->concepts) &&
+             conceptsWrite(&conceptsFPA, fpa, chip, cell, source, db, fpa->concepts)) || !propagate);
+}
+
+
+bool pmConceptsInit(void)
+{
+    bool init = false;                  // Did we initialise anything?
+    if (! conceptsFPA) {
+        conceptsFPA = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+
+        #if 0
+        // FPA.NAME
+        {
+            psMetadataItem *fpaName = psMetadataItemAllocStr("FPA.NAME", "Name of FPA", "");
+            pmConceptRegister(fpaName, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaName);
+        }
+        #endif
+
+        // FPA.AIRMASS
+        {
+            psMetadataItem *fpaAirmass = psMetadataItemAllocF32("FPA.AIRMASS", "Airmass at boresight", 0.0);
+            pmConceptRegister(fpaAirmass, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaAirmass);
+        }
+
+        // FPA.FILTER
+        {
+            psMetadataItem *fpaFilter = psMetadataItemAllocStr("FPA.FILTER", "Filter used", "");
+            pmConceptRegister(fpaFilter, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaFilter);
+        }
+
+        // FPA.POSANGLE
+        {
+            psMetadataItem *fpaPosangle = psMetadataItemAllocF32("FPA.POSANGLE",
+                                          "Position angle of instrument", 0.0);
+            pmConceptRegister(fpaPosangle, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaPosangle);
+        }
+
+        // FPA.RADECSYS
+        {
+            psMetadataItem *fpaRadecsys = psMetadataItemAllocStr("FPA.RADECSYS",
+                                          "Celestial coordinate system", "");
+            pmConceptRegister(fpaRadecsys, NULL, NULL, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaRadecsys);
+        }
+
+        // FPA.RA
+        {
+            psMetadataItem *fpaRa = psMetadataItemAllocF64("FPA.RA", "Right Ascension of boresight", NAN);
+            pmConceptRegister(fpaRa, (pmConceptParseFunc)pmConceptParse_FPA_Coords,
+                              (pmConceptFormatFunc)pmConceptFormat_FPA_Coords, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaRa);
+        }
+
+        // FPA.DEC
+        {
+            psMetadataItem *fpaDec = psMetadataItemAllocF64("FPA.DEC", "Declination of boresight", NAN);
+            pmConceptRegister(fpaDec, (pmConceptParseFunc)pmConceptParse_FPA_Coords,
+                              (pmConceptFormatFunc)pmConceptFormat_FPA_Coords, PM_CONCEPT_LEVEL_FPA);
+            psFree(fpaDec);
+        }
+
+        // Done with FPA level concepts
+    }
+    if (! conceptsChip) {
+        conceptsChip = psMetadataAlloc();
+        init = true;
+        // There are no standard concepts at the chip level to be installed
+    }
+    if (! conceptsCell) {
+        conceptsCell = psMetadataAlloc();
+        init = true;
+
+        // Install the standard concepts
+
+        // CELL.GAIN
+        {
+            psMetadataItem *cellGain = psMetadataItemAllocF32("CELL.GAIN", "CCD gain (e/count)", NAN);
+            pmConceptRegister(cellGain, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellGain);
+        }
+
+        // CELL.READNOISE
+        {
+            psMetadataItem *cellReadnoise = psMetadataItemAllocF32("CELL.READNOISE",
+                                            "CCD read noise (e)", NAN);
+            pmConceptRegister(cellReadnoise, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellReadnoise);
+        }
+
+        // CELL.SATURATION
+        {
+            psMetadataItem *cellSaturation = psMetadataItemAllocF32("CELL.SATURATION",
+                                             "Saturation level (counts)", NAN);
+            pmConceptRegister(cellSaturation, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellSaturation);
+        }
+
+        // CELL.BAD
+        {
+            psMetadataItem *cellBad = psMetadataItemAllocF32("CELL.BAD", "Bad level (counts)", NAN);
+            pmConceptRegister(cellBad, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellBad);
+        }
+
+        // CELL.XPARITY
+        {
+            psMetadataItem *cellXparity = psMetadataItemAllocS32("CELL.XPARITY",
+                                          "Orientation in x compared to the rest of the FPA", 0);
+            pmConceptRegister(cellXparity, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellXparity);
+        }
+
+        // CELL.YPARITY
+        {
+            psMetadataItem *cellYparity = psMetadataItemAllocS32("CELL.YPARITY",
+                                          "Orientation in x compared to the rest of the FPA", 0);
+            pmConceptRegister(cellYparity, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellYparity);
+        }
+
+        // CELL.READDIR
+        {
+            psMetadataItem *cellReaddir = psMetadataItemAllocS32("CELL.READDIR",
+                                          "Read direction, rows=1, cols=2", 0);
+            pmConceptRegister(cellReaddir, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellReaddir);
+        }
+
+
+        // These (CELL.EXPOSURE and CELL.DARKTIME) used to be READOUT.EXPOSURE and READOUT.DARKTIME, 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
+        {
+            psMetadataItem *cellExposure = psMetadataItemAllocF32("CELL.EXPOSURE",
+                                           "Exposure time (sec)", NAN);
+            pmConceptRegister(cellExposure, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellExposure);
+        }
+
+        // CELL.DARKTIME
+        {
+            psMetadataItem *cellDarktime = psMetadataItemAllocF32("CELL.DARKTIME",
+                                           "Time since flush (sec)", NAN);
+            pmConceptRegister(cellDarktime, NULL, NULL, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellDarktime);
+        }
+
+        // CELL.TRIMSEC
+        {
+            psRegion *trimsec = psAlloc(sizeof(psRegion)); // Blank trimsec
+            trimsec->x0 = trimsec->y0 = trimsec->x1 = trimsec->y1 = NAN;
+            psMetadataItem *cellTrimsec = psMetadataItemAllocPtr("CELL.TRIMSEC", PS_DATA_REGION,
+                                          "Trim section", trimsec);
+            psFree(trimsec);
+            pmConceptRegister(cellTrimsec, (pmConceptParseFunc)pmConceptParse_CELL_TRIMSEC,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_TRIMSEC, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellTrimsec);
+        }
+
+        // CELL.BIASSEC
+        {
+            psList *biassecs = psListAlloc(NULL); // Blank biassecs
+            psMetadataItem *cellBiassec = psMetadataItemAllocPtr("CELL.BIASSEC", PS_DATA_LIST,
+                                          "Bias sections", biassecs);
+            psFree(biassecs);
+            pmConceptRegister(cellBiassec, (pmConceptParseFunc)pmConceptParse_CELL_BIASSEC,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_BIASSEC, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellBiassec);
+        }
+
+        // CELL.XBIN
+        {
+            psMetadataItem *cellXbin = psMetadataItemAllocS32("CELL.XBIN", "Binning in x", 0);
+            pmConceptRegister(cellXbin, (pmConceptParseFunc)pmConceptParse_CELL_Binning,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_XBIN, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellXbin);
+        }
+
+        // CELL.YBIN
+        {
+            psMetadataItem *cellYbin = psMetadataItemAllocS32("CELL.YBIN", "Binning in y", 0);
+            pmConceptRegister(cellYbin, (pmConceptParseFunc)pmConceptParse_CELL_Binning,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_YBIN, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellYbin);
+        }
+
+        // CELL.TIMESYS
+        {
+            psMetadataItem *cellTimesys = psMetadataItemAllocS32("CELL.TIMESYS", "Time system", -1);
+            pmConceptRegister(cellTimesys, (pmConceptParseFunc)pmConceptParse_CELL_TIMESYS,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_TIMESYS, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellTimesys);
+        }
+
+        // CELL.TIME
+        {
+            psTime *time = psTimeAlloc(PS_TIME_TAI); // Blank time
+            // Not particularly distinguishing, but should be good enough
+            time->sec = 0;
+            time->nsec = 0;
+            psMetadataItem *cellTime = psMetadataItemAlloc("CELL.TIME", PS_DATA_TIME,
+                                       "Time of exposure", time);
+            psFree(time);
+            pmConceptRegister(cellTime, (pmConceptParseFunc)pmConceptParse_CELL_TIME,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_TIME, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellTime);
+        }
+
+        // CELL.X0
+        {
+            psMetadataItem *cellX0 = psMetadataItemAllocS32("CELL.X0", "Position of (0,0) on the chip", 0);
+            pmConceptRegister(cellX0, (pmConceptParseFunc)pmConceptParse_CELL_Positions,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_Positions, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellX0);
+        }
+
+        // CELL.Y0
+        {
+            psMetadataItem *cellY0 = psMetadataItemAllocS32("CELL.Y0", "Position of (0,0) on the chip", 0);
+            pmConceptRegister(cellY0, (pmConceptParseFunc)pmConceptParse_CELL_Positions,
+                              (pmConceptFormatFunc)pmConceptFormat_CELL_Positions, PM_CONCEPT_LEVEL_CELL);
+            psFree(cellY0);
+        }
+
+    }
+
+    conceptsInitialised = true;
+
+    return init;
+}
+
+void pmConceptsDone(void)
+{
+    psFree(conceptsFPA);
+    psFree(conceptsChip);
+    psFree(conceptsCell);
+}
+
+
+// Copy concepts from one FPA to another
+bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
+                       pmFPA *source    // The target FPA
+                      )
+{
+    // Copy FPA concepts
+    target->concepts = psMetadataCopy(target->concepts, source->concepts);
+
+    // Copy chip concepts
+    psArray *targetChips = target->chips; // Chips in target
+    psArray *sourceChips = source->chips; // Chips in source
+    if (targetChips->n != sourceChips->n) {
+        psError(PS_ERR_IO, true, "Number of chips in target (%d) and source (%d) differ --- unable to copy "
+                "concepts.\n", targetChips->n, sourceChips->n);
+        return false;
+    }
+    for (int i = 0; i < targetChips->n; i++) {
+        pmChip *targetChip = targetChips->data[i]; // Target chip of interest
+        pmChip *sourceChip = sourceChips->data[i]; // Source chip of interest
+        if (! targetChip || ! sourceChip) {
+            continue;
+        }
+        targetChip->concepts = psMetadataCopy(targetChip->concepts, sourceChip->concepts);
+
+        // Copy cell concepts
+        psArray *targetCells = targetChip->cells; // Cells in target
+        psArray *sourceCells = sourceChip->cells; // Cells in source
+        if (targetCells->n != sourceCells->n) {
+            psError(PS_ERR_IO, true, "Number of cells in target (%d) and source (%d) differ for chip %d ---"
+                    " unable to copy concepts.\n", targetCells->n, sourceCells->n, i);
+            return false;
+        }
+        for (int j = 0; j < targetCells->n; j++) {
+            pmCell *targetCell = targetCells->data[j]; // Target chip of interest
+            pmCell *sourceCell = sourceCells->data[j]; // Source chip of interest
+            if (! targetCell || ! sourceCell) {
+                continue;
+            }
+            targetCell->concepts = psMetadataCopy(targetCell->concepts, sourceCell->concepts);
+        }
+    }
+
+    return true;
+}
Index: trunk/psModules/src/astrom/pmConcepts.h
===================================================================
--- trunk/psModules/src/astrom/pmConcepts.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmConcepts.h	(revision 6872)
@@ -0,0 +1,99 @@
+#ifndef PM_CONCEPTS_H
+#define PM_CONCEPTS_H
+
+#include "pslib.h"
+
+#include "pmFPA.h"
+
+
+// Function to call to parse a concept once it has been read
+typedef psMetadataItem* (*pmConceptParseFunc)(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+// Function to call to format a concept for writing
+typedef psMetadataItem* (*pmConceptFormatFunc)(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+
+// A "concept" specification
+typedef struct
+{
+    psMetadataItem *blank;              // Blank value of concept; also contains the name
+    pmConceptParseFunc parse;           // Function to call to read the concept
+    pmConceptFormatFunc format;         // Function to call to write the concept
+}
+pmConceptSpec;
+
+// Allocator
+pmConceptSpec *pmConceptSpecAlloc(psMetadataItem *blank, // Blank value; contains the name
+                                  pmConceptParseFunc parse, // Function to call to parse the concept
+                                  pmConceptFormatFunc format // Function to call to format the concept
+                                 );
+
+// Level at which to store a concept in the FPA hierarchy
+typedef enum {
+    PM_CONCEPT_LEVEL_FPA,               // Store in the FPA
+    PM_CONCEPT_LEVEL_CHIP,              // Store in the chip
+    PM_CONCEPT_LEVEL_CELL               // Store in the cell
+} pmConceptLevel;
+
+// Register a new concept
+bool pmConceptRegister(psMetadataItem *blank, // Blank value; contains the name
+                       pmConceptParseFunc parse, // Function to call to parse the concept
+                       pmConceptFormatFunc format, // Function to call to format the concept
+                       pmConceptLevel level // Level at which to store concept in the FPA hierarchy
+                      );
+
+// Some specificity to reading and writing concepts
+typedef enum {
+    PM_CONCEPT_SOURCE_ALL      = 0x00,  // All concepts
+    PM_CONCEPT_SOURCE_CAMERA   = 0x01,  // Concept comes from the camera information
+    PM_CONCEPT_SOURCE_DEFAULTS = 0x02,  // Concept comes from defaults
+    PM_CONCEPT_SOURCE_HEADER   = 0x04,  // Concept comes from FITS header
+    PM_CONCEPT_SOURCE_DATABASE = 0x08   // Concept comes from database
+} pmConceptSource;
+
+// Set blanks, read or write concepts at the appropriate level
+bool pmConceptsBlankFPA(pmFPA *fpa      // FPA for which to set blank concepts
+                       );
+bool pmConceptsReadFPA(pmFPA *fpa,      // FPA for which to read concepts
+                       pmConceptSource source, // Source for concepts
+                       psDB *db         // Database handle
+                      );
+bool pmConceptsWriteFPA(pmFPA *fpa,     // FPA for which to write concepts
+                        pmConceptSource source, // Source for concepts
+                        psDB *db        // Database handle
+                       );
+bool pmConceptsBlankChip(pmChip *chip   // FPA for which to set blank concepts
+                        );
+bool pmConceptsReadChip(pmChip *chip,   // Chip for which to read concepts
+                        pmConceptSource source, // Source for concepts
+                        bool propagate, // Propagate to higher levels as well?
+                        psDB *db        // Database handle
+                       );
+bool pmConceptsWriteChip(pmChip *chip,  // Chip for which to write concepts
+                         pmConceptSource source, // Source for concepts
+                         bool propagate,// Propagate to higher levels as well?
+                         psDB *db       // Database handle
+                        );
+bool pmConceptsBlankCell(pmCell *cell   // Cell for which to set blank concepts
+                        );
+bool pmConceptsReadCell(pmCell *cell,   // Cell for which to read concepts
+                        pmConceptSource source, // Source for concepts
+                        bool propagate, // Propagate to higher levels as well?
+                        psDB *db        // Database handle
+                       );
+bool pmConceptsWriteCell(pmCell *cell,  // FPA for which to write concepts
+                         pmConceptSource source, // Source for concepts
+                         bool propagate,// Propagate to higher levels as well?
+                         psDB *db       // Database handle
+                        );
+
+// Set up the blank concepts
+bool pmConceptsInit(void);
+// Free the concept specs so there's no memory leak
+void pmConceptsDone(void);
+
+// Copy all the concepts within an FPA to another FPA
+bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
+                       pmFPA *source    // The target FPA
+                      );
+
+
+#endif
Index: trunk/psModules/src/astrom/pmConceptsRead.c
===================================================================
--- trunk/psModules/src/astrom/pmConceptsRead.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsRead.c	(revision 6872)
@@ -0,0 +1,606 @@
+#include <stdio.h>
+
+#include "pslib.h"
+#include "psMetadataItemParse.h"
+
+#include "pmFPA.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// This function gets called for the really boring concepts --- where all you have to do is parse from a
+// header or database and you don't need to muck around with conversions.  There is no similar "formatPlain",
+// since the type is already known.
+static psMetadataItem *parsePlain(psMetadataItem *concept, // The concept to parse
+                                  psMetadataItem *pattern // The concept pattern
+                                 )
+{
+    switch (pattern->type) {
+    case PS_DATA_STRING: {
+            psString string = psMetadataItemParseString(concept); // Get the string, so I can free it after it
+            // goes on the MetadataItem
+            psMetadataItem *item = psMetadataItemAllocStr(pattern->name, pattern->comment, string);
+            psFree(string);
+            return item;
+        }
+    case PS_DATA_S32:
+        return psMetadataItemAllocS32(pattern->name, pattern->comment, psMetadataItemParseS32(concept));
+    case PS_DATA_F32:
+        return psMetadataItemAllocF32(pattern->name, pattern->comment, psMetadataItemParseF32(concept));
+    case PS_DATA_F64:
+        return psMetadataItemAllocF64(pattern->name, pattern->comment, psMetadataItemParseF64(concept));
+    default:
+        psLogMsg(__func__, PS_LOG_WARN, "Concept %s (%s) is not of a standard type (%x)\n",
+                 pattern->name, pattern->comment, pattern->type);
+        return NULL;
+    }
+}
+
+
+// Parse a single concept
+static bool conceptParse(pmConceptSpec *spec, // The concept specification
+                         psMetadataItem *concept, // The concept to parse
+                         psMetadata *cameraFormat, // The camera format
+                         psMetadata *target, // The target
+                         pmFPA *fpa,    // The FPA
+                         pmChip *chip,  // The chip
+                         pmCell *cell   // The cell
+                        )
+{
+    if (concept) {
+        psMetadataItem *parsed = NULL;  // The parsed concept
+        if (spec->parse) {
+            parsed = spec->parse(concept, spec->blank, cameraFormat, fpa, chip, cell);
+        } else {
+            parsed = parsePlain(concept, spec->blank);
+        }
+
+        // Reformat so that everything's clean
+        if (strcmp(spec->blank->name, parsed->name) != 0 ||
+                strcmp(spec->blank->comment, parsed->comment) != 0) {
+            psMetadataItem *cleaned = NULL;     // Item that's been cleaned up --- correct name and comment
+            switch (spec->blank->type) {
+            case PS_DATA_STRING:
+                cleaned = psMetadataItemAllocStr(spec->blank->name, spec->blank->comment, parsed->data.V);
+                break;
+            case PS_DATA_S32:
+                cleaned = psMetadataItemAllocS32(spec->blank->name, spec->blank->comment, parsed->data.S32);
+                break;
+            case PS_DATA_F32:
+                cleaned = psMetadataItemAllocF32(spec->blank->name, spec->blank->comment, parsed->data.F32);
+                break;
+            case PS_DATA_F64:
+                cleaned = psMetadataItemAllocF64(spec->blank->name, spec->blank->comment, parsed->data.F64);
+                break;
+            default:
+                cleaned = psMetadataItemAlloc(spec->blank->name, concept->type, spec->blank->comment,
+                                              parsed->data.V);
+            }
+            psFree(parsed);
+            parsed = cleaned;
+        }
+        psMetadataAddItem(target, parsed, PS_LIST_TAIL, PS_META_REPLACE);
+        psFree(parsed);     // Drop reference
+        return true;
+    }
+
+    return false;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmConceptsReadFromCamera(psMetadata *specs, // The concept specifications
+                              pmCell *cell,  // The cell
+                              psMetadata *target // Place into which to read the concepts
+                             )
+{
+    if (cell) {
+        pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+        if (! hdu) {
+            return false;
+        }
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadata *cellConfig = cell->config; // The camera configuration for this cell
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *conceptItem = psMetadataLookup(cellConfig, name); // The concept, or NULL
+            psMetadataItem *value = NULL; // The value of the concept
+            if (conceptItem) {
+                if (conceptItem->type == PS_DATA_STRING) {
+                    // Check the SOURCE
+                    psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+                    psStringAppend(&nameSource, "%s.SOURCE", name);
+                    bool mdok = true;       // Status of MD lookup
+                    psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+                    psFree(nameSource);
+                    if (mdok && strlen(source) > 0 && strcasecmp(source, "VALUE") == 0) {
+                        value = conceptItem;
+                        conceptParse(spec, value, cameraFormat, target, NULL, NULL, cell);
+                    } else if (source && (strlen(source) == 0 || strcasecmp(source, "HEADER") != 0)) {
+                        // We leave "HEADER" to pmConceptsReadFromHeader
+                        psError(PS_ERR_IO, true, "%s isn't HEADER or VALUE --- can't read %s\n", source,
+                                name);
+                        continue;
+                    }
+                } else {
+                    // Another type --- should be OK
+                    conceptParse(spec, conceptItem, cameraFormat, target, NULL, NULL, cell);
+                }
+            }
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+
+bool pmConceptsReadFromDefaults(psMetadata *specs, // The concept specifications
+                                pmFPA *fpa, // The FPA
+                                pmChip *chip, // The chip
+                                pmCell *cell, // The cell
+                                psMetadata *target // Place into which to read the concepts
+                               )
+{
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
+    if (mdok && defaults) {
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *conceptItem = psMetadataLookup(defaults, name); // The concept, or NULL
+            conceptParse(spec, conceptItem, cameraFormat, target, fpa, chip, cell);
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+
+bool pmConceptsReadFromHeader(psMetadata *specs, // The concept specifications
+                              pmFPA *fpa, // The FPA
+                              pmChip *chip, // The chip
+                              pmCell *cell,  // The cell
+                              psMetadata *target // Place into which to read the concepts
+                             )
+{
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *transSpec = psMetadataLookupMD(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
+    if (mdok && transSpec) {
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *headerItem = NULL; // The value of the concept from the header
+            // First check the cell configuration
+            if (cell && cell->config) {
+                psMetadataItem *conceptItem = psMetadataLookup(cell->config, name); // The concept, or NULL
+                if (conceptItem) {
+                    // Check the SOURCE
+                    psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+                    psStringAppend(&nameSource, "%s.SOURCE", name);
+                    psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+                    psFree(nameSource);
+                    if (mdok && strlen(source) && strcasecmp(source, "HEADER") == 0) {
+                        headerItem = psMetadataLookup(hdu->header, conceptItem->data.V);
+                    }
+                    // Leave the error handling to pmConceptsFromCamera, which should already have been called
+                }
+            }
+            if (! headerItem) {
+                psString keywords = psMetadataLookupStr(&mdok, transSpec, name); // The FITS keywords
+                if (mdok && strlen(keywords) > 0) {
+                    // In case there are multiple headers
+                    psList *keys = psStringSplit(keywords, " ,;"); // List of keywords
+                    if (keys->n == 1) {
+                        // Only one key --- proceed as usual
+                        headerItem = psMetadataLookup(hdu->header, keywords);
+                    } else {
+                        psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator
+                        psString key = NULL; // Item from iteration
+                        psList *values = psListAlloc(NULL); // List containing the values
+                        while ((key = psListGetAndIncrement(keysIter))) {
+                            psMetadataItem *value = psMetadataLookup(hdu->header, key);
+                            psListAdd(values, PS_LIST_TAIL, value);
+                        }
+                        psFree(keysIter);
+                        headerItem = psMetadataItemAlloc(name, PS_DATA_LIST, specItem->comment, values);
+                        psFree(values);
+                    }
+                    psFree(keys);
+                }
+            }
+
+            // This will also clean up the name
+            conceptParse(spec, headerItem, cameraFormat, target, fpa, chip, cell);
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+
+// XXX --- the below code has NOT been tested!
+bool pmConceptsReadFromDatabase(psMetadata *specs, // The concept specifications
+                                pmFPA *fpa, // The FPA
+                                pmChip *chip, // The chip
+                                pmCell *cell,  // The cell
+                                psDB *db, // The database handle
+                                psMetadata *target // Place into which to read the concepts
+                               )
+{
+    pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *dbSpec = psMetadataLookupMD(&mdok, cameraFormat, "DATABSE"); // The DATABASE spec
+    if (mdok && dbSpec) {
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+
+            psMetadata *dbLookup = psMetadataLookupMD(&mdok, dbSpec, name);
+            if (mdok && dbLookup) {
+                const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE"); // Table name
+                // Names of the "where" columns
+                const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
+                // Values of the "where" columns
+                const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");
+
+                // Now, need to get the "given"s
+                if (strlen(givenCols) > 0 || strlen(givenPS) > 0) {
+                    psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+                    psList *values = psStringSplit(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", name);
+                    } 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 *dependName = 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", name);
+                            } else {
+                                // Search for the value name
+                                psMetadataItem *item = NULL; // The value
+                                if (!item && cell) {
+                                    item = psMetadataLookup(cell->concepts, dependName);
+                                }
+                                if (!item && chip) {
+                                    item = psMetadataLookup(chip->concepts, dependName);
+                                }
+                                if (!item && fpa) {
+                                    item = psMetadataLookup(fpa->concepts, dependName);
+                                }
+                                if (! item) {
+                                    psLogMsg(__func__, PS_LOG_ERROR, "Unable to find the value name %s for DB"
+                                             " lookup on %s --- ignored.\n", dependName, name);
+                                } 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(name, item->type,
+                                                              item->comment,
+                                                              item->data.V);
+                                    psMetadataAddItem(selection, newItem, PS_LIST_TAIL, PS_META_REPLACE);
+                                    psFree(newItem);
+                                }
+                            }
+                            psFree(dependName);
+                            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 *conceptItem = 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", name);
+                        } else {
+                            if (dbResult-> n > 1) {
+                                psLogMsg(__func__, PS_LOG_WARN,
+                                         "Multiple rows returned in DB lookup for %s --- "
+                                         " using the first one only.\n", name);
+                            }
+                            conceptItem = (psMetadataItem*)dbResult->data[0];
+                        }
+
+                        // Now we have the result
+                        conceptParse(spec, conceptItem, cameraFormat, target, fpa, chip, cell);
+
+                    }
+                    psFree(cols);
+                    psFree(values);
+                }
+            } // Doing the "given"s.
+
+        } // Iterating through the concept specifications
+        psFree(specsIter);
+
+        return true;
+    }
+    return false;
+}
+
+
+
+
+#ifdef OLD
+
+psMetadataItem *pmConceptReadFromCamera(pmCell *cell, // The cell
+                                        const char *concept // Name of concept
+                                       )
+{
+    if (cell) {
+        psMetadata *camera = cell->config; // Camera configuration
+        psMetadataItem *item = psMetadataLookup(camera, concept);
+        return item;
+    }
+    return NULL;
+}
+
+psMetadataItem *pmConceptReadFromHeader(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 = psMetadataLookupStr(&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;
+            }
+        }
+    }
+
+    // No header value
+    return NULL;
+}
+
+
+// Look for a default
+psMetadataItem *pmConceptReadFromDefault(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 = psMetadataLookupStr(&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 = pmConceptReadFromHeader(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 *pmConceptReadFromDB(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 = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
+        // const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+        // columns
+        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+        // columns
+
+        // Now, need to get the "given"s
+        if (strlen(givenCols) || strlen(givenPS)) {
+            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+            psList *values = psStringSplit(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 = pmConceptReadFromHeader(fpa, chip, cell, name);
+                        if (! item) {
+                            item = pmConceptReadFromDefault(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");
+    return NULL;
+}
+
+
+// Concept lookup
+psMetadataItem *pmConceptRead(pmFPA *fpa, // The FPA
+                              pmChip *chip,// The chip
+                              pmCell *cell, // The cell
+                              psDB *db, // DB handle
+                              const char *name // Concept name
+                             )
+{
+    // Try headers, database, defaults in order
+    psMetadataItem *item = pmConceptReadFromCamera(cell, name);
+    if (! item) {
+        item = pmConceptReadFromHeader(fpa, chip, cell, name);
+    }
+    if (! item) {
+        item = pmConceptReadFromDB(fpa, chip, cell, db, name);
+    }
+    if (! item) {
+        item = pmConceptReadFromDefault(fpa, chip, cell, name);
+    }
+    return item; // item is either NULL, or points to what was desired
+}
+
+
+#endif
Index: trunk/psModules/src/astrom/pmConceptsRead.h
===================================================================
--- trunk/psModules/src/astrom/pmConceptsRead.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsRead.h	(revision 6872)
@@ -0,0 +1,94 @@
+#ifndef PM_CONCEPTS_READ_H
+#define PM_CONCEPTS_READ_H
+
+#include "pmFPA.h"
+
+
+bool pmConceptsReadFromCamera(psMetadata *specs, // The concept specifications
+                              pmCell *cell,  // The cell
+                              psMetadata *target // Place into which to read the concepts
+                             );
+bool pmConceptsReadFromDefaults(psMetadata *specs, // The concept specifications
+                                pmFPA *fpa, // The FPA
+                                pmChip *chip, // The chip
+                                pmCell *cell, // The cell
+                                psMetadata *target // Place into which to read the concepts
+                               );
+bool pmConceptsReadFromHeader(psMetadata *specs, // The concept specifications
+                              pmFPA *fpa, // The FPA
+                              pmChip *chip, // The chip
+                              pmCell *cell,  // The cell
+                              psMetadata *target // Place into which to read the concepts
+                             );
+bool pmConceptsReadFromDatabase(psMetadata *specs, // The concept specifications
+                                pmFPA *fpa, // The FPA
+                                pmChip *chip, // The chip
+                                pmCell *cell,  // The cell
+                                psDB *db, // The database handle
+                                psMetadata *target // Place into which to read the concepts
+                               );
+
+
+
+
+#ifdef OLD
+psMetadataItem *pmConceptReadFromCamera(pmCell *cell, // The cell
+                                        const char *concept // Name of concept
+                                       );
+
+psMetadataItem *pmConceptReadFromHeader(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 *pmConceptReadFromDefault(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 *pmConceptReadFromDB(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 *pmConceptRead(pmFPA *fpa, // The FPA
+                              pmChip *chip,// The chip
+                              pmCell *cell, // The cell
+                              psDB *db, // DB handle
+                              const char *concept // Concept name
+                             );
+
+float pmConceptReadF32(pmFPA *fpa,        // The FPA
+                       pmChip *chip,      // The chip
+                       pmCell *cell,      // The cell
+                       psDB *db,          // DB handle
+                       const char *name // Name of the concept
+                      );
+
+double pmConceptReadF64(pmFPA *fpa,   // The FPA
+                        pmChip *chip, // The chip
+                        pmCell *cell, // The cell
+                        psDB *db,     // DB handle
+                        const char *name // Name of the concept
+                       );
+
+int pmConceptReadS32(pmFPA *fpa,   // The FPA
+                     pmChip *chip, // The chip
+                     pmCell *cell, // The cell
+                     psDB *db,     // DB handle
+                     const char *name // Name of the concept
+                    );
+
+psString pmConceptReadString(pmFPA *fpa, // The FPA
+                             pmChip *chip, // The chip
+                             pmCell *cell, // The cell
+                             psDB *db,  // DB handle
+                             const char *name // Name of the concept
+                            );
+#endif
+
+#endif
Index: trunk/psModules/src/astrom/pmConceptsStandard.c
===================================================================
--- trunk/psModules/src/astrom/pmConceptsStandard.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsStandard.c	(revision 6872)
@@ -0,0 +1,610 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmConceptsRead.h"
+#include "pmConceptsWrite.h"
+#include "pmConceptsStandard.h"
+
+
+#define COMPARE_REGIONS(a,b) (((a)->x0 == (b)->x0 && \
+                               (a)->x1 == (b)->x1 && \
+                               (a)->y0 == (b)->y0 && \
+                               (a)->y1 == (b)->y1) ? true : false)
+
+
+static double defaultCoordScaling(psMetadataItem *pattern)
+{
+    if (strcmp(pattern->name, "FPA.RA") == 0) {
+        psLogMsg(__func__, PS_LOG_WARN, "Assuming format for %s is HOURS.\n", pattern->name);
+        return M_PI / 12.0;
+    }
+    if (strcmp(pattern->name, "FPA.DEC") == 0) {
+        psLogMsg(__func__, PS_LOG_WARN, "Assuming format for %s is DEGREES.\n", pattern->name);
+        return M_PI / 180.0;
+    }
+    psAbort(__func__, "Should never ever get here.\n");
+    return NAN;
+}
+
+
+// FPA.RA and FPA.DEC
+psMetadataItem *pmConceptParse_FPA_Coords(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+    assert(cameraFormat);
+
+    double coords = NAN;                // The coordinates
+    switch (concept->type) {
+    case PS_TYPE_F32:
+        coords = concept->data.F32;
+        break;
+    case PS_TYPE_F64:
+        coords = concept->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(concept->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
+                    sscanf(concept->data.V, "%d %d %f", &big, &medium, &small) != 3)
+            {
+                psError(PS_ERR_IO, true, "Cannot interpret FPA.RA: %s\n", concept->data.V);
+                break;
+            }
+            coords = abs(big) + (float)medium/60.0 + small/3600.0;
+            if (big < 0)
+            {
+                coords *= -1.0;
+            }
+        }
+        break;
+    default:
+        psError(PS_ERR_IO, true, "%s concept is of an unexpected type: %x\n", pattern->name, concept->type);
+        return NULL;
+    }
+
+    // How to interpret the coordinates
+    bool mdok = true;           // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, pattern->name);
+        if (mdok && strlen(format) > 0) {
+            if (strcasecmp(format, "HOURS") == 0) {
+                coords *= M_PI / 12.0;
+            } else if (strcasecmp(format, "DEGREES") == 0) {
+                coords *= M_PI / 180.0;
+            } else if (strcasecmp(format, "RADIANS") == 0) {
+                // No action required
+            } else {
+                coords *= defaultCoordScaling(pattern);
+            }
+        } else {
+            coords *= defaultCoordScaling(pattern);
+        }
+    } else {
+        coords *= defaultCoordScaling(pattern);
+    }
+
+    return psMetadataItemAllocF64(pattern->name, pattern->comment, coords);
+}
+
+// FPA.RA and FPA.DEC
+psMetadataItem *pmConceptFormat_FPA_Coords(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat)
+{
+    assert(concept);
+    assert(pattern);
+    assert(cameraFormat);
+
+    double coords = concept->data.F64;  // The coordinates
+
+    // How to interpret the coordinates
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, pattern->name);
+        if (mdok && strlen(format) > 0) {
+            if (strcasecmp(format, "HOURS") == 0) {
+                coords /= M_PI / 12.0;
+            } else if (strcasecmp(format, "DEGREES") == 0) {
+                coords /= M_PI / 180.0;
+            } else if (strcasecmp(format, "RADIANS") == 0) {
+                // No action required
+            } else {
+                coords /= defaultCoordScaling(pattern);
+            }
+        } else {
+            coords /= defaultCoordScaling(pattern);
+        }
+    } else {
+        coords /= defaultCoordScaling(pattern);
+    }
+
+    // We choose to write sexagesimal format
+    int big, medium;
+    float small;
+    big = (int)coords;
+    medium = (int)(60.0*(coords - (double)big));
+    small = 3600.0*(coords - (double)big - 60.0 * (double)medium);
+    psString coordString = NULL;        // String with the coordinates in sexagesimal format
+    psStringAppend(&coordString, "%d:%d:%.2f", big, medium, small);
+    psMetadataItem *coordItem = psMetadataItemAllocStr(pattern->name, pattern->comment, coordString);
+    psFree(coordString);
+
+    return coordItem;
+}
+
+
+psMetadataItem *pmConceptParse_CELL_TRIMSEC(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(cell);
+    assert(pattern);
+
+    psRegion *trimsec = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed by value)
+
+    if (concept->type != PS_DATA_STRING) {
+        psError(PS_ERR_IO, true, "CELL.TRIMSEC after read is not of type STR (%x)\n", concept->type);
+        *trimsec = psRegionSet(0.0, 0.0, 0.0, 0.0);
+    } else {
+        *trimsec = psRegionFromString(concept->data.V);
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_REGION, pattern->comment, trimsec);
+    psFree(trimsec);
+    return item;
+}
+
+psMetadataItem *pmConceptParse_CELL_BIASSEC(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(cell);
+    assert(pattern);
+
+    psList *biassecs = psListAlloc(NULL); // List of bias sections
+
+    switch (concept->type) {
+    case PS_DATA_STRING: {
+            psList *regions = psStringSplit(concept->data.V, " ;"); // List of regions
+            psListIterator *regionsIter = psListIteratorAlloc(regions, PS_LIST_HEAD, false); // Iterator
+            psString regionString = NULL; // Region string from iteration
+            while ((regionString = psListGetAndIncrement(regionsIter))) {
+                psRegion *region = psAlloc(sizeof(psRegion)); // The region
+                *region = psRegionFromString(regionString);
+                psListAdd(biassecs, PS_LIST_TAIL, region);
+                psFree(region);           // Drop reference
+            }
+            psFree(regionsIter);
+            psFree(regions);
+            break;
+        }
+    case PS_DATA_LIST: {
+            psList *regions = concept->data.V; // The list of regions
+            psListIterator *regionsIter = psListIteratorAlloc(regions, PS_LIST_HEAD, false); // Iterator
+            psMetadataItem *regionItem = NULL; // Item from list iteration
+            while ((regionItem = psListGetAndIncrement(regionsIter))) {
+                if (regionItem->type != PS_DATA_STRING) {
+                    psLogMsg(__func__, PS_LOG_WARN, "CELL.BIASSEC member is not of type STR --- ignored.\n");
+                    continue;
+                }
+                psRegion *region = psAlloc(sizeof(psRegion)); // The region
+                *region = psRegionFromString(regionItem->data.V);
+                psListAdd(biassecs, PS_LIST_TAIL, region);
+                psFree(region);           // Drop reference
+            }
+            psFree(regionsIter);
+            break;
+        }
+    default:
+        psError(PS_ERR_IO, true, "CELL.BIASSEC after read is not of type STRING or LIST --- assuming "
+                "blank.\n");
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_LIST, pattern->comment, biassecs);
+    psFree(biassecs);               // Drop reference
+    return item;
+}
+
+// CELL.XBIN and CELL.YBIN
+psMetadataItem *pmConceptParse_CELL_Binning(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+
+    int binning = 1;                    // Binning factor in x
+    if (concept->type == PS_DATA_STRING) {
+        psString binString = concept->data.V; // The string containing the binning
+        if ((strcmp(pattern->name, "CELL.XBIN") == 0 && sscanf(binString, "%d %*d", &binning) != 1 &&
+                sscanf(binString, "%d,%*d", &binning) != 1) ||
+                (strcmp(pattern->name, "CELL.YBIN") == 0 && sscanf(binString, "%*d %d", &binning) != 1 &&
+                 sscanf(binString, "%*d,%d", &binning) != 1)) {
+            psError(PS_ERR_IO, true, "Unable to parse string to get %s: %s\n", pattern->name, binString);
+        }
+    } else if (concept->type == PS_TYPE_S32) {
+        binning = concept->data.S32;
+    } else {
+        psError(PS_ERR_IO, true, "Note sure how to parse %s of type %x --- assuming 1.\n", pattern->name,
+                concept->type);
+    }
+
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, binning);
+}
+
+
+psMetadataItem *pmConceptParse_CELL_TIMESYS(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(pattern);
+
+    psTimeType timeSys = PS_TIME_UTC;   // The time system
+    psString sys = concept->data.V;     // The time system string
+    if (concept->type != PS_DATA_STRING || strlen(sys) <= 0) {
+        psError(PS_ERR_IO, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
+    } else 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");
+    }
+
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, timeSys);
+}
+
+
+psMetadataItem *pmConceptParse_CELL_TIME(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+    assert(cell);
+
+    // Need CELL.TIMESYS first
+    bool mdok = true;                  // Result of MD lookup
+    psTimeType timeSys = psMetadataLookupS32(&mdok, cell->concepts, "CELL.TIMESYS"); // The time system
+    if (!mdok) {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to find CELL.TIMESYS in concepts --- assuming UTC.\n");
+        timeSys = PS_TIME_UTC;
+    }
+
+    // Get format
+    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
+    if (!mdok || !formats) {
+        psError(PS_ERR_IO, false, "Unable to find FORMATS in camera configuration.\n");
+        return NULL;
+    }
+
+    psString timeFormat = psMetadataLookupStr(&mdok, formats, "CELL.TIME");
+    if (!mdok || strlen(timeFormat) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find CELL.TIME in FORMATS.\n");
+        return NULL;
+    }
+
+    psTime *time = NULL;                // The time
+    switch (concept->type) {
+    case PS_DATA_LIST: {
+            // The date and time are stored separately
+            // Assume the date is first and the time second
+            psList *dateTime = concept->data.V; // The list containing items for date and time
+            psMetadataItem *dateItem = psListGet(dateTime, PS_LIST_HEAD); // Item containing the date
+            psMetadataItem *timeItem = psListGet(dateTime, PS_LIST_HEAD + 1); // Item containing the time
+            if (dateItem->type != PS_DATA_STRING) {
+                psError(PS_ERR_IO, true, "Date is not of type STR.\n");
+                return NULL;
+            }
+            psString dateString = dateItem->data.V; // The string with the date
+            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);
+                return NULL;
+            }
+            if (strstr(timeFormat, "BACKWARDS")) {
+                int temp = day;
+                day = year;
+                year = temp;
+            }
+            if (strstr(timeFormat, "PRE2000") || year < 2000) {
+                year += 2000;
+            }
+
+            psString timeString = NULL; // The string with the time
+            if (timeItem->type == PS_DATA_STRING) {
+                timeString = 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 is not of an expected type: %x\n", timeItem->type);
+                    return NULL;
+                }
+                // 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(&timeString, "%02d:%02d:%02f", hours, minutes, seconds);
+            }
+            psString dateTimeString = NULL;
+            psStringAppend(&dateTimeString, "%sT%s", dateString, timeString);
+            time = psTimeFromISO(dateTimeString, timeSys);
+            break;
+        }
+    case PS_DATA_STRING: {
+            psString timeString = concept->data.V;   // String with the time
+            if (strcasecmp(timeFormat, "ISO") == 0) {
+                // timeString contains an ISO time
+                time = psTimeFromISO(timeString, timeSys);
+            } else if (strcasecmp(timeFormat, "JD") == 0) {
+                double timeValue = strtod (timeString, NULL);
+                time = psTimeFromJD(timeValue);
+            } else if (strcasecmp(timeFormat, "MJD") == 0) {
+                double timeValue = strtod (timeString, NULL);
+                time = psTimeFromMJD(timeValue);
+            } 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)concept->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)concept->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");
+        return NULL;
+    }
+
+    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_TIME, pattern->comment, time);
+    psFree(time);
+    return item;
+}
+
+// Correct a position --- in case the user wants FORTRAN indexing (like the FITS standard...)
+static int fortranCorr(psMetadata *cameraFormat, // The camera format description
+                       const char *name // Name of concept to check for FORTRAN indexing
+                      )
+{
+    bool mdok = false;                  // Result of MD lookup
+    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
+    if (mdok && formats) {
+        psString format = psMetadataLookupStr(&mdok, formats, name);
+        if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
+            return 1;
+        }
+    }
+    return 0;
+}
+
+psMetadataItem *pmConceptParse_CELL_Positions(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    if (concept->type != PS_TYPE_S32) {
+        psError(PS_ERR_IO, true, "Concept %s is not of type S32, as expected.\n", pattern->name);
+        return NULL;
+    }
+    int offset = concept->data.S32;
+    offset -= fortranCorr(cameraFormat, pattern->name);
+    return psMetadataItemAllocS32(pattern->name, pattern->comment, offset);
+}
+
+
+psMetadataItem *pmConceptFormat_CELL_TRIMSEC(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+
+    psRegion *trimsec = concept->data.V; // The trimsec region
+    psString trimsecString = psRegionToString(*trimsec);
+    psMetadataItem *formatted = psMetadataItemAllocStr(concept->name, concept->comment,
+                                trimsecString);
+    psFree(trimsecString);
+    return formatted;
+}
+
+psMetadataItem *pmConceptFormat_CELL_BIASSEC(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    // Return a metadata item containing a list of metadata items of region strings
+    psList *biassecs = concept->data.V; // The biassecs region list
+    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+    psRegion *region = NULL;            // Region from iteration
+    psList *new = psListAlloc(NULL);    // New list containing metadatas
+    while ((region = psListGetAndIncrement(biassecsIter))) {
+        psString regionString = psRegionToString(*region); // The string region "[x0:x1,y0:y1]"
+        psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, regionString);
+        psFree(regionString);           // Drop reference
+        psListAdd(new, PS_LIST_TAIL, item);
+        psFree(item);                   // Drop reference
+    }
+    psFree(biassecsIter);
+    psMetadataItem *formatted = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment, new);
+    psFree(new);                        // Drop reference
+    return formatted;
+}
+
+// This function actually does both CELL.XBIN and CELL.YBIN if CELL.XBIN and CELL.YBIN are specified by the
+// same header.
+psMetadataItem *pmConceptFormat_CELL_XBIN(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+
+    psMetadata *translation = psMetadataLookupMD(NULL, cameraFormat, "TRANSLATION");
+    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
+    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
+    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
+    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+            strcasecmp(xKeyword, yKeyword) == 0) {
+        psMetadataItem *yBinItem = psMetadataLookup(cell->concepts, "CELL.YBIN"); // Binning factor in y
+        psString binString = psStringCopy("");
+        psStringAppend(&binString, "%d %d", concept->data.S32, yBinItem->data.S32);
+        psMetadataItem *binItem = psMetadataItemAllocStr(concept->name, concept->comment, binString);
+        psFree(binString);
+        return binItem;
+    }
+
+    // Otherwise, there's no formatting required
+    return psMemIncrRefCounter(concept);
+}
+
+// Only need to format if both if CELL.XBIN and CELL.YBIN are not specified by the same header.
+psMetadataItem *pmConceptFormat_CELL_YBIN(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+
+    psMetadata *translation = psMetadataLookupMD(NULL, cameraFormat, "TRANSLATION");
+    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
+    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
+    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
+    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
+            strcasecmp(xKeyword, yKeyword) == 0) {
+        // Censor this --- it's already done (though no harm if it's done twice
+        return NULL;
+    }
+
+    // No formatting required
+    return psMemIncrRefCounter(concept);
+}
+
+
+psMetadataItem *pmConceptFormat_CELL_TIMESYS(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    psString sys = NULL;            // String to store
+    switch (concept->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(concept->name, concept->comment, sys);
+    psFree(sys);
+
+    return newItem;
+}
+
+psMetadataItem *pmConceptFormat_CELL_TIME(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    psTime *time = concept->data.V;     // The time
+    psMetadata *formats = psMetadataLookupMD(NULL, cameraFormat, "FORMATS");
+    psString format = psMetadataLookupStr(NULL, formats, "CELL.TIME");
+
+    if (strlen(format) == 0 || strcasecmp(format, "ISO") == 0) {
+        psString dateTimeString = psTimeToISO(time); // String representation
+        psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, dateTimeString);
+        psFree(dateTimeString);
+        return item;
+    }
+    if (strstr(format, "SEPARATE")) {
+        // We're working with two separate headers --- construct a list with the date and time separately
+        psString dateTimeString = psTimeToISO(time); // String representation
+        psList *dateTime = psStringSplit(dateTimeString, "T");
+        psFree(dateTimeString);
+        psString dateString = psListGet(dateTime, PS_LIST_HEAD); // The date string
+        psString timeString = psListGet(dateTime, PS_LIST_TAIL); // The time string
+
+        // Need to format the strings....
+        // 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");
+            return NULL;
+        }
+        if (strstr(format, "BACKWARDS")) {
+            psError(PS_ERR_IO, true, "You want it BACKWARDS?  Not right now, thanks.\n");
+            return NULL;
+        }
+
+        psMetadataItem *dateItem = psMetadataItemAllocStr(concept->name, "The date of observation",
+                                   dateString);
+        psMetadataItem *timeItem = psMetadataItemAllocStr(concept->name, "The time of observation",
+                                   timeString);
+        psFree(dateString);
+        psFree(timeString);
+
+        psListRemove(dateTime, PS_LIST_HEAD);
+        psListRemove(dateTime, PS_LIST_HEAD);
+        psListAdd(dateTime, PS_LIST_HEAD, dateItem);
+        psListAdd(dateTime, PS_LIST_TAIL, timeItem);
+
+        psMetadataItem *item = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment,
+                               dateTime);
+        return item;
+    }
+    if (strcasecmp(format, "MJD") == 0) {
+        double mjd = psTimeToMJD(time);
+        return psMetadataItemAllocF64(concept->name, concept->comment, mjd);
+    }
+    if (strcasecmp(format, "JD") == 0) {
+        double jd = psTimeToMJD(time);
+        return psMetadataItemAllocF64("CELL.TIME", "JD of observation", jd);
+    }
+
+    psError(PS_ERR_IO, true, "Not sure how to format concept CELL.TIME\n");
+    return NULL;
+}
+
+psMetadataItem *pmConceptFormat_CELL_Positions(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
+{
+    assert(concept);
+    assert(cameraFormat);
+
+    if (concept->type != PS_TYPE_S32) {
+        psError(PS_ERR_IO, true, "Concept %s is not of type S32, as expected.\n", concept->name);
+        return NULL;
+    }
+    int offset = concept->data.S32;
+    offset += fortranCorr(cameraFormat, concept->name);
+    return psMetadataItemAllocS32(concept->name, concept->comment, offset);
+}
+
Index: trunk/psModules/src/astrom/pmConceptsStandard.h
===================================================================
--- trunk/psModules/src/astrom/pmConceptsStandard.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsStandard.h	(revision 6872)
@@ -0,0 +1,24 @@
+#ifndef PM_CONCEPTS_STANDARD_H
+#define PM_CONCEPTS_STANDARD_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+
+psMetadataItem *pmConceptParse_FPA_Coords(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_FPA_Coords(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat);
+psMetadataItem *pmConceptParse_CELL_TRIMSEC(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptParse_CELL_BIASSEC(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptParse_CELL_Binning(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptParse_CELL_TIMESYS(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptParse_CELL_TIME(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptParse_CELL_Positions(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_TRIMSEC(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_BIASSEC(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_XBIN(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_YBIN(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_TIMESYS(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_TIME(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+psMetadataItem *pmConceptFormat_CELL_Positions(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell);
+
+#endif
Index: trunk/psModules/src/astrom/pmConceptsWrite.c
===================================================================
--- trunk/psModules/src/astrom/pmConceptsWrite.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsWrite.c	(revision 6872)
@@ -0,0 +1,870 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "pmConceptsRead.h"
+#include "psAdditionals.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static bool compareConcepts(psMetadataItem *compare, // Item to compare
+                            psMetadataItem *standard // Standard for comparison
+                           )
+{
+    // First order checks
+    if (! compare || ! standard) {
+        return false;
+    }
+    if (strcasecmp(compare->name, standard->name) != 0) {
+        return false;
+    }
+
+    // Check the more boring types
+    switch (compare->type) {
+    case PS_TYPE_S32:
+        switch (standard->type) {
+        case PS_TYPE_S32:
+            psTrace(__func__, 10, "Comparing %d and %d...\n", compare->data.S32, standard->data.S32);
+            return (compare->data.S32 == standard->data.S32) ? true : false;
+        case PS_TYPE_F32:
+            psTrace(__func__, 10, "Comparing %d and %d...\n", compare->data.S32, (int)standard->data.F32);
+            return (compare->data.S32 == (int)standard->data.F32) ? true : false;
+        case PS_TYPE_F64:
+            psTrace(__func__, 10, "Comparing %d and %d...\n", compare->data.S32, (int)standard->data.F64);
+            return (compare->data.S32 == (int)standard->data.F64) ? true : false;
+        default:
+            return false;
+        }
+    case PS_TYPE_F32:
+        switch (standard->type) {
+        case PS_TYPE_S32:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, (float)standard->data.S32);
+            return (compare->data.F32 == (float)standard->data.S32) ? true : false;
+        case PS_TYPE_F32:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, standard->data.F32);
+            return (compare->data.F32 == standard->data.F32) ? true : false;
+        case PS_TYPE_F64:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, (float)standard->data.F64);
+            return (compare->data.F32 == (float)standard->data.F64) ? true : false;
+        default:
+            return false;
+        }
+    case PS_TYPE_F64:
+        switch (standard->type) {
+        case PS_TYPE_S32:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, (double)standard->data.S32);
+            return (compare->data.F64 == (double)standard->data.S32) ? true : false;
+        case PS_TYPE_F32:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, (double)standard->data.F32);
+            return (compare->data.F64 == (double)standard->data.F32) ? true : false;
+        case PS_TYPE_F64:
+            psTrace(__func__, 10, "Comparing %f and %f...\n", compare->data.F32, standard->data.F64);
+            return (compare->data.F64 == standard->data.F64) ? true : false;
+        default:
+            return false;
+        }
+        return (compare->data.F64 == standard->data.F64) ? true : false;
+    case PS_DATA_STRING:
+        if (standard->type != PS_DATA_STRING) {
+            return false;
+        }
+        psTrace(__func__, 10, "Comparing '%s' and '%s'\n", compare->data.V, standard->data.V);
+        return (strcasecmp(compare->data.V, standard->data.V) == 0) ? true : false;
+    case PS_DATA_LIST: {              // A list of strings
+            // "compare" contains a list of psMetadataItems
+            // "standard" likely contains just a string (but it might possibly be a list of strings)
+            psList *cList = compare->data.V; // The list from comparison item
+            psList *sList = NULL;         // The list from standard item
+            switch (standard->type) {
+            case PS_DATA_STRING:
+                sList = psStringSplit(standard->data.V, " ;");
+                break;
+            case PS_DATA_LIST:
+                sList = psMemIncrRefCounter(standard->data.V);
+                break;
+            default:
+                return false;
+            }
+            if (cList->n != sList->n) {
+                psFree(sList);
+                return false;
+            }
+            psVector *match = psVectorAlloc(cList->n, PS_TYPE_U8); // Array indicating which values match
+            psVectorInit(match, 0);
+            psListIterator *cIter = psListIteratorAlloc(cList, PS_LIST_HEAD, false); // compare iterator
+            psListIterator *sIter = psListIteratorAlloc(sList, PS_LIST_HEAD, false); // standard iterator
+            psMetadataItem *cItem = NULL; // Item from compare list
+            while ((cItem = psListGetAndIncrement(cIter))) {
+                if (cItem->type != PS_DATA_STRING) {
+                    psLogMsg(__func__, PS_LOG_WARN, "psMetadataItem from list is of type %x instead of "
+                             "%x (PS_DATA_STRING) --- can't interpret.\n", cItem->type, PS_DATA_STRING);
+                    psFree(cIter);
+                    psFree(sIter);
+                    psFree(match);
+                    psFree(sList);
+                    return false;
+                }
+                psString cString = cItem->data.V; // String from compare list
+                psListIteratorSet(sIter, PS_LIST_HEAD);
+                int index = 0;            // Index for list
+                bool found = false;       // Found a match?
+                for (psString sString = NULL; (sString = psListGetAndIncrement(sIter)) && !found; index++) {
+                    if (strcasecmp(cString, sString) == 0) {
+                        match->data.U8[index]++;
+                        found = true;
+                    }
+                }
+                if (! found) {
+                    // Can give up immediately
+                    psFree(cIter);
+                    psFree(sIter);
+                    psFree(match);
+                    psFree(sList);
+                    return false;
+                }
+            }
+            // Make sure we got 100% matches in both directions
+            bool allMatch = true;         // Did all of them match?
+            for (int i = 0; i < match->n && allMatch; i++) {
+                if (!match->data.U8[i]) {
+                    allMatch = false;
+                }
+            }
+            psFree(cIter);
+            psFree(sIter);
+            psFree(sList);
+            psFree(match);
+            return allMatch;
+        }
+    default:
+        return false;
+    }
+    psAbort(__func__, "Should never get here.\n");
+}
+
+
+// Format a single concept
+static psMetadataItem *conceptFormat(pmConceptSpec *spec, // The concept specification
+                                     psMetadataItem *concept, // The concept to parse
+                                     psMetadata *cameraFormat, // The camera format
+                                     pmFPA *fpa, // The FPA
+                                     pmChip *chip, // The chip
+                                     pmCell *cell // The cell
+                                    )
+{
+    if (concept) {
+        psMetadataItem *formatted = NULL;  // The formatted concept
+        if (spec->format) {
+            formatted = spec->format(concept, cameraFormat, fpa, chip, cell);
+        } else {
+            formatted = psMemIncrRefCounter(concept);
+        }
+
+        return formatted;
+    }
+    return NULL;
+}
+
+// Write a single value to a header
+static bool writeSingleHeader(pmHDU *hdu, // HDU for which to add to the header
+                              const char *keyword, // Keyword to add
+                              psMetadataItem *item // Item to add to the header
+                             )
+{
+    switch (item->type) {
+    case PS_DATA_STRING:
+        return psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.V);
+    case PS_DATA_S32:
+        return psMetadataAddS32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.S32);
+    case PS_DATA_F32:
+        return psMetadataAddF32(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.F32);
+    case PS_DATA_F64:
+        return psMetadataAddF64(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                item->data.F64);
+    case PS_DATA_REGION: {
+            psString region = psRegionToString(*(psRegion*)item->data.V);
+            bool result = psMetadataAddStr(hdu->header, PS_LIST_TAIL, keyword, PS_META_REPLACE, item->comment,
+                                           region);
+            psFree(region);
+            return result;
+        }
+    default:
+        psLogMsg(__func__, PS_LOG_WARN, "Type of %s is not suitable for a FITS header --- not added.\n",
+                 item->name);
+        return false;
+    }
+}
+
+
+// Write potentially multiple values to a header
+static bool writeHeader(pmHDU *hdu,     // HDU for which to add to the header
+                        const char *keywords, // Keywords to add
+                        psMetadataItem *item // Item to add to the header
+                       )
+{
+    bool status = true;                 // Status of writing headers, to be returned
+    if (item->type == PS_DATA_LIST) {
+        psList *values = item->data.V;  // List of outputs
+        psList *keys = psStringSplit(keywords, " ,;"); // List of keywords
+        if (keys->n != values->n) {
+            psLogMsg(__func__, PS_LOG_WARN, "Number of keywords (%d) does not match number of values (%d).\n",
+                     keys->n, values->n);
+        }
+        psListIterator *keysIter = psListIteratorAlloc(keys, PS_LIST_HEAD, false); // Iterator for keywords
+        psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
+        psString key = NULL;            // Keyword from iteration
+        psMetadataItem *value = NULL;   // Value from iteration
+        while ((key = psListGetAndIncrement(keysIter)) && (value = psListGetAndIncrement(valuesIter))) {
+            status |= writeSingleHeader(hdu, key, value);
+        }
+        psFree(keysIter);
+        psFree(valuesIter);
+        psFree(keys);
+    } else {
+        status = writeSingleHeader(hdu, keywords, item);
+    }
+    return status;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmConceptsWriteToCamera(psMetadata *specs, // The concept specifications
+                             pmCell *cell,   // The cell
+                             psMetadata *concepts // The concepts
+                            )
+{
+    if (cell) {
+        pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+        if (!hdu) {
+            return false;
+        }
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *cameraItem = psMetadataLookup(cell->config, name); // The concept from the camera,
+            // or NULL
+            if (cameraItem) {
+                // Grab the concept
+                psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The concept
+                // Formatted version
+                psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, NULL, NULL, cell);
+                if (!formatted) {
+                    continue;
+                }
+                psString nameSource = NULL; // String with the concept name and ".SOURCE" added
+                psStringAppend(&nameSource, "%s.SOURCE", name);
+                bool mdok = true;       // Status of MD lookup
+                psString source = psMetadataLookupStr(&mdok, cell->config, nameSource); // The source
+                if (mdok && strlen(source) > 0) {
+                    psTrace(__func__, 8, "%s is %s\n", nameSource, source);
+                    if (strcasecmp(source, "HEADER") == 0) {
+                        if (cameraItem->type != PS_DATA_STRING) {
+                            psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by header, but is not "
+                                     "of type STR --- ignored.\n", conceptItem->name);
+                            continue;
+                        }
+                        psTrace(__func__, 8, "Writing %s to header %s\n", name, cameraItem->data.V);
+                        writeHeader(hdu, cameraItem->data.V, formatted);
+                    } else if (strcasecmp(source, "VALUE") == 0) {
+                        psTrace(__func__, 8, "Checking %s against camera format.\n", name);
+                        if (! compareConcepts(formatted, cameraItem)) {
+                            psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by value in the camera "
+                                     "format, but the values don't match.\n", name);
+                        }
+                    } else {
+                        psLogMsg(__func__, PS_LOG_WARN, "Concept source %s isn't HEADER or VALUE --- can't "
+                                 "write\n", nameSource);
+                    }
+                } else if (! compareConcepts(formatted, cameraItem)) {
+                    // Assume it's specified by value
+                    psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by value in the camera "
+                             "format, but the values don't match.\n", name);
+                }
+                psFree(formatted);
+                psFree(nameSource);
+            }
+
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+bool pmConceptsWriteToDefaults(psMetadata *specs, // The concept specifications
+                               pmFPA *fpa, // The FPA
+                               pmChip *chip, // The chip
+                               pmCell *cell, // The cell
+                               psMetadata *concepts // The concepts
+                              )
+{
+    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *defaults = psMetadataLookupMD(&mdok, cameraFormat, "DEFAULTS"); // The DEFAULTS spec
+    if (mdok && defaults) {
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *defaultItem = psMetadataLookup(defaults, name); // The item from the DEFAULTS
+            if (defaultItem) {
+                psMetadataItem *conceptItem = NULL; // The item from the concepts
+                if (defaultItem->type == PS_DATA_METADATA) {
+                    // It's a menu --- need to look up the .DEPEND
+                    psString dependName = NULL; // The concept name with ".DEPEND" on the end
+                    psStringAppend(&dependName, ".DEPEND");
+                    psString dependKey = psMetadataLookupStr(&mdok, defaults, dependName); // The keyword
+                    psFree(dependName);
+                    if (!mdok || !dependKey || strlen(dependKey) == 0) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Can't find %s in the DEFAULTS for %s --- ignored.\n",
+                                 dependName, name);
+                        continue;
+                    }
+                    psString dependValue = psMetadataLookupStr(&mdok, concepts, dependName); // The value
+                    if (!mdok || !dependKey || strlen(dependKey) == 0) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Concept %s specified by %s isn't of type STR -- "
+                                 "ignored.\n", name, dependName);
+                        continue;
+                    }
+                    conceptItem = psMetadataLookup(defaultItem->data.V, dependValue);
+                } else {
+                    conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+                }
+                psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
+                if (!formatted) {
+                    continue;
+                }
+                if (! compareConcepts(formatted, defaultItem)) {
+                    psLogMsg(__func__, PS_LOG_WARN, "Concept %s is specified by the DEFAULTS in the camera "
+                             "format, but the values don't match.\n", name);
+                }
+                psFree(formatted);
+            }
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+
+bool pmConceptsWriteToHeader(psMetadata *specs, // The concept specifications
+                             pmFPA *fpa, // The FPA
+                             pmChip *chip, // The chip
+                             pmCell *cell, // The cell
+                             psMetadata *concepts // The concepts
+                            )
+{
+    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *translation = psMetadataLookupMD(&mdok, cameraFormat, "TRANSLATION"); // The TRANSLATION spec
+    if (mdok && translation) {
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+            psMetadataItem *headerItem = psMetadataLookup(translation, name); // The item from the TRANSLATION
+            if (headerItem) {
+                if (headerItem->type != PS_DATA_STRING) {
+                    psLogMsg(__func__, PS_LOG_WARN, "TRANSLATION keyword for concept %s isn't of type STR ---"
+                             " ignored.", name);
+                    continue;
+                }
+                psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+                psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
+                if (!formatted) {
+                    continue;
+                }
+                psList *keywords = psStringSplit(headerItem->data.V, " ,;"); // List of header keywords
+                if (formatted->type == PS_DATA_LIST) {
+                    psList *values = formatted->data.V; // The values for the headers
+                    if (values->n != keywords->n) {
+                        psLogMsg(__func__, PS_LOG_WARN, "Number of headers specified does not match number "
+                                 "of values for concept %s.\n", name);
+                    }
+                    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator
+                    psListIterator *keywordsIter = psListIteratorAlloc(keywords, PS_LIST_HEAD, false);
+                    psMetadataItem *valuesItem = NULL; // Item from list
+                    while ((valuesItem = psListGetAndIncrement(valuesIter))) {
+                        psString keyword = psListGetAndIncrement(keywordsIter); // Keyword from the list
+                        if (strlen(keyword) > 0) {
+                            writeHeader(hdu, keyword, formatted);
+                        }
+                    }
+                    psFree(valuesIter);
+                    psFree(keywordsIter);
+                } else {
+                    psString keyword = psListGet(keywords, PS_LIST_HEAD); // The keyword
+                    writeHeader(hdu, keyword, formatted);
+                }
+                psFree(formatted);
+                psFree(keywords);
+            }
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+// XXX Warning: This code has not been tested at all
+bool pmConceptsWriteToDatabase(psMetadata *specs, // The concept specifications
+                               pmFPA *fpa, // The FPA
+                               pmChip *chip, // The chip
+                               pmCell *cell, // The cell
+                               psDB *db,// The database handle
+                               psMetadata *concepts // The concepts
+                              )
+{
+    pmHDU *hdu = pmHDUGetLowest(NULL, NULL, cell); // The HDU at the lowest level
+    if (!hdu) {
+        return false;
+    }
+    psMetadata *cameraFormat = hdu->format; // The camera format
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *database = psMetadataLookupMD(&mdok, cameraFormat, "DATABASE"); // The DATABASE spec
+    if (mdok && database) {
+        pmHDU *hdu = pmHDUGetLowest(fpa, chip, cell); // The HDU at the lowest level
+        psMetadata *cameraFormat = hdu->format; // The camera format
+        psMetadataIterator *specsIter = psMetadataIteratorAlloc(specs, PS_LIST_HEAD, NULL); // Iterator
+        psMetadataItem *specItem = NULL;    // Item from the specs metadata
+        while ((specItem = psMetadataGetAndIncrement(specsIter))) {
+            pmConceptSpec *spec = specItem->data.V; // The specification
+            psString name = specItem->name; // The concept name
+
+            psMetadataItem *dbItem = psMetadataLookup(database, name); // The item from the DATABASE
+            if (dbItem) {
+                if (dbItem->type != PS_DATA_METADATA) {
+                    psLogMsg(__func__, PS_LOG_WARN, "DATABASE keyword for concept %s isn't of type METADATA "
+                             "--- ignored.\n", name);
+                    continue;
+                }
+
+                psMetadataItem *conceptItem = psMetadataLookup(concepts, name); // The item from the concepts
+                psMetadataItem *formatted = conceptFormat(spec, conceptItem, cameraFormat, fpa, chip, cell);
+                if (!formatted) {
+                    continue;
+                }
+
+                psMetadata *dbLookup = dbItem->data.V; // How to look up the value of interest
+                // Name of the table
+                const char *tableName = psMetadataLookupStr(&mdok, dbLookup, "TABLE");
+                // Name of "where" columns
+                const char *givenCols = psMetadataLookupStr(&mdok, dbLookup, "GIVENDBCOL");
+                // Values for "where" columns
+                const char *givenPS = psMetadataLookupStr(&mdok, dbLookup, "GIVENPS");
+
+                // Now, need to get the "given"s
+                if (strlen(givenCols) || strlen(givenPS)) {
+                    psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+                    psList *values = psStringSplit(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", name);
+                    } 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 *dependName = 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", name);
+                            } else {
+                                // Search for the value name
+                                psMetadataItem *item = NULL; // The value
+                                if (!item && cell) {
+                                    item = psMetadataLookup(cell->concepts, dependName);
+                                }
+                                if (!item && chip) {
+                                    item = psMetadataLookup(chip->concepts, dependName);
+                                }
+                                if (!item && fpa) {
+                                    item = psMetadataLookup(fpa->concepts, dependName);
+                                }
+                                if (! item) {
+                                    psLogMsg(__func__, PS_LOG_ERROR,
+                                             "Unable to find the value name %s for DB "
+                                             " lookup on %s --- ignored.\n", dependName, name);
+                                } 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
+                                    psMetadataAddItem(selection, formatted, PS_LIST_TAIL, PS_META_REPLACE);
+                                    psFree(formatted);
+                                }
+                            }
+                            psFree(dependName);
+                            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
+                        if (! dbResult || dbResult->n == 0) {
+                            psLogMsg(__func__, PS_LOG_WARN, "Unable to find any rows in DB for %s --- "
+                                     "ignored\n", name);
+                            return false;
+                        } else {
+                            if (dbResult->n > 1) {
+                                psLogMsg(__func__, PS_LOG_WARN, "Multiple rows returned in DB lookup for %s "
+                                         "--- ignored.\n", name);
+                            }
+                            // Update the DB
+                            psMetadata *update = psMetadataAlloc();
+                            psMetadataAddItem(update, conceptItem, PS_LIST_HEAD, 0);
+                            psDBUpdateRows(db, tableName, selection, update);
+                            psFree(update);
+                            return true;
+                        }
+                    }
+                    psFree(cols);
+                    psFree(values);
+                } // Doing the "given"s.
+            }
+        }
+        psFree(specsIter);
+        return true;
+    }
+    return false;
+}
+
+
+
+
+#if 0
+
+// Well, not really "write", but check to make sure it's there and matches
+bool pmConceptWriteToCamera(pmCell *cell, // The cell
+                            psMetadataItem *concept // Concept
+                           )
+{
+    if (! cell->config) {
+        return false;
+    }
+    if (cell) {
+        psMetadataItem *item = psMetadataLookup(cell->config, concept->name); // Info we want
+        return compareConcepts(item, concept);
+    }
+
+    return false;
+}
+
+// Write the concept to the header in the appropriate location
+bool pmConceptWriteToHeader(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
+    if (! fpa->camera) {
+        return false;
+    }
+    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 = psMetadataLookupStr(&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;
+        }
+        psFree(headerItem);
+    }
+
+    // No header value
+    return status;
+}
+
+
+// Well, not really "write", but check to see if it's there, and matches
+bool pmConceptWriteToDefault(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
+    if (! fpa->camera) {
+        return false;
+    }
+    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 = psMetadataLookupStr(&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 = pmConceptReadFromHeader(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
+// XXX I WOULD NOT TRUST THIS FUNCTION IN THE SLIGHTEST YET! --- PAP
+bool pmConceptWriteToDB(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
+    if (! fpa->camera) {
+        return false;
+    }
+    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 = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
+        //        const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+        // columns
+        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+        // columns
+
+        // Now, need to get the "given"s
+        if (strlen(givenCols) || strlen(givenPS)) {
+            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+            psList *values = psStringSplit(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 = pmConceptReadFromHeader(fpa, chip, cell, name);
+                        if (! item) {
+                            item = pmConceptReadFromDefault(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
+                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");
+    return false;
+}
+
+
+// Concept write from item
+bool pmConceptWriteItem(pmFPA *fpa,     // The FPA
+                        pmChip *chip,   // The chip
+                        pmCell *cell,   // The cell
+                        psDB *db,       // DB handle
+                        psMetadataItem *concept // Concept item
+                       )
+{
+    if (! fpa->camera) {
+        return false;
+    }
+
+    // Try headers, database, defaults in order
+    psTrace(__func__, 3, "Trying to set concept %s...\n", concept->name);
+    bool status = pmConceptWriteToCamera(cell, concept); // Status for return
+    if (! status) {
+        psTrace(__func__, 5, "Trying header....\n");
+        status = pmConceptWriteToHeader(fpa, chip, cell, concept);
+    }
+    if (! status) {
+        psTrace(__func__, 5, "Trying database....\n");
+        status = pmConceptWriteToDB(fpa, chip, cell, db, concept);
+    }
+    if (! status) {
+        psTrace(__func__, 5, "Checking defaults....\n");
+        status = pmConceptWriteToDefault(fpa, chip, cell, concept);
+    }
+
+    if (! status) {
+        psError(PS_ERR_IO, true, "Unable to set %s (%s).\n", concept->name, concept->comment);
+    }
+
+    return status;
+}
+
+
+// Concept write
+bool pmConceptWrite(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 pmConceptWriteItem(fpa, chip, cell, db, concept);
+}
+
+#endif
Index: trunk/psModules/src/astrom/pmConceptsWrite.h
===================================================================
--- trunk/psModules/src/astrom/pmConceptsWrite.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmConceptsWrite.h	(revision 6872)
@@ -0,0 +1,80 @@
+#ifndef PM_CONCEPTS_WRITE_H
+#define PM_CONCEPTS_WRITE_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+
+
+bool pmConceptsWriteToCamera(psMetadata *specs, // The concept specifications
+                             pmCell *cell,   // The cell
+                             psMetadata *concepts // The concepts
+                            );
+bool pmConceptsWriteToDefaults(psMetadata *specs, // The concept specifications
+                               pmFPA *fpa, // The FPA
+                               pmChip *chip, // The chip
+                               pmCell *cell, // The cell
+                               psMetadata *concepts // The concepts
+                              );
+bool pmConceptsWriteToHeader(psMetadata *specs, // The concept specifications
+                             pmFPA *fpa, // The FPA
+                             pmChip *chip, // The chip
+                             pmCell *cell, // The cell
+                             psMetadata *concepts // The concepts
+                            );
+bool pmConceptsWriteToDatabase(psMetadata *specs, // The concept specifications
+                               pmFPA *fpa, // The FPA
+                               pmChip *chip, // The chip
+                               pmCell *cell, // The cell
+                               psDB *db,// The database handle
+                               psMetadata *concepts // The concepts
+                              );
+
+
+#ifdef OLD
+
+// Well, not really "write", but check to make sure it's there and matches
+bool pmConceptWriteToCamera(pmCell *cell, // The cell
+                            psMetadataItem *concept // Concept
+                           );
+
+// Write the concept to the header in the appropriate location
+bool pmConceptWriteToHeader(pmFPA *fpa, // The FPA that contains the chip
+                            pmChip *chip, // The chip that contains the cell
+                            pmCell *cell, // The cell
+                            psMetadataItem *concept // Concept
+                           );
+
+// Well, not really "write", but check to see if it's there, and matches
+bool pmConceptWriteToDefault(pmFPA *fpa, // The FPA that contains the chip
+                             pmChip *chip, // The chip that contains the cell
+                             pmCell *cell, // The cell
+                             psMetadataItem *concept // Concept
+                            );
+
+bool pmConceptWriteToDB(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
+                       );
+
+// Concept write from item
+bool pmConceptWriteItem(pmFPA *fpa,     // The FPA
+                        pmChip *chip,   // The chip
+                        pmCell *cell,   // The cell
+                        psDB *db,       // DB handle
+                        psMetadataItem *concept // Concept item
+                       );
+
+bool pmConceptWrite(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
+                   );
+
+#endif
+
+#endif
Index: trunk/psModules/src/astrom/pmFPA.c
===================================================================
--- trunk/psModules/src/astrom/pmFPA.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPA.c	(revision 6872)
@@ -0,0 +1,649 @@
+/** @file  pmFPA.c
+*
+*  @brief This file defines the basic types for the FPA hierarchy
+*
+*  @ingroup AstroImage
+*
+*  @author GLG, MHPCC
+*
+* XXX: We should review the extent of the warning messages on these functions
+* when the transformations are not successful.
+*
+* XXX: Should we implement non-linear cell->chip transforms?
+*
+*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include "pslib.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmConcepts.h"
+#include "pmMaskBadPixels.h"
+
+/******************************************************************************
+ *****************************************************************************/
+#define PS_FREE_HIERARCHY 1
+#define PARENT_LINKS 0
+
+static void readoutFree(pmReadout *readout)
+{
+    psTrace(__func__, 9, "Removing readout %lx from cell %lx...\n", (size_t) readout, (size_t) readout->parent);
+    if (readout->parent) {
+        psArray *readouts = readout->parent->readouts;
+        for (int i = 0; i < readouts->n; i++) {
+            if (readouts->data[i] == readout) {
+                //                    pmReadout *tmpReadout = readouts->data[i];
+                readouts->data[i] = NULL;
+                #if PARENT_LINKS
+
+                psFree(tmpReadout);
+                #endif
+
+                break;
+            }
+        }
+    }
+    psTrace(__func__, 9, "Freeing readout %lx\n", (size_t) readout);
+
+    #if PARENT_LINKS
+
+    psFree(readout->parent);
+    #endif
+
+    psFree(readout->image);
+    psFree(readout->mask);
+    psFree(readout->weight);
+    psFree(readout->analysis);
+    psFree(readout->bias);
+}
+
+static void cellFree(pmCell *cell)
+{
+    psTrace(__func__, 9, "Removing cell %lx from chip %lx...\n", (size_t)cell, (size_t)cell->parent);
+    if (cell->parent) {
+        psArray *cells = cell->parent->cells;
+        for (int i = 0; i < cells->n; i++) {
+            if (cells->data[i] == cell) {
+                //                    pmCell *tmpCell = cells->data[i];
+                cells->data[i] = NULL;
+                #if PARENT_LINKS
+
+                psFree(tmpCell);
+                #endif
+
+                break;
+            }
+        }
+    }
+    psTrace(__func__, 9, "Freeing cell %lx\n", (size_t)cell);
+
+    pmCellFreeReadouts(cell);
+    psFree(cell->readouts);
+    #if PARENT_LINKS
+
+    psFree(cell->parent);
+    #endif
+
+    psFree(cell->concepts);
+    psFree(cell->analysis);
+    psFree(cell->config);
+    psFree(cell->hdu);
+}
+
+static void chipFree(pmChip* chip)
+{
+    psTrace(__func__, 9, "Removing chip %lx from fpa %lx...\n", (size_t)chip, (size_t)chip->parent);
+    if (chip->parent) {
+        psArray *chips = chip->parent->chips;
+        for (int i = 0; i < chips->n; i++) {
+            if (chips->data[i] == chip) {
+                //                    pmChip *tmpChip = chips->data[i];
+                chips->data[i] = NULL;
+                #if PARENT_LINKS
+
+                psFree(tmpChip);
+                #endif
+
+                break;
+            }
+        }
+    }
+    psTrace(__func__, 9, "Freeing chip %lx\n", (size_t)chip);
+
+    pmChipFreeCells(chip);
+    psFree(chip->cells);
+    #if PARENT_LINKS
+
+    psFree(chip->parent);
+    #endif
+
+    #if 0
+
+    psFree(chip->toFPA);
+    psFree(chip->fromFPA);
+    #endif
+
+    psFree(chip->concepts);
+    psFree(chip->analysis);
+    psFree(chip->hdu);
+}
+
+
+static void FPAFree(pmFPA *fpa)
+{
+    psTrace(__func__, 9, "Freeing fpa %lx\n", (size_t)fpa);
+    #if 0
+
+    psFree(fpa->fromTangentPlane);
+    psFree(fpa->toTangentPlane);
+    psFree(fpa->projection);
+    #endif
+
+    psFree(fpa->concepts);
+    psFree(fpa->analysis);
+    psFree(fpa->camera);
+    //
+    // Set the parent to NULL in all fpa->chips before psFree(fpa->chips)
+    // in order to avoid memory reference counter problems.
+    //
+    psArray *chips = fpa->chips;
+    for (psS32 i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = chips->data[i];
+        if (! tmpChip) {
+            continue;
+        }
+        chips->data[i] = NULL;
+        tmpChip->parent = NULL;
+        if (PS_FREE_HIERARCHY == 1) {
+            psFree(tmpChip);
+        }
+    }
+    psFree(fpa->chips);
+    psFree(fpa->hdu);
+}
+
+void pmCellFreeReadouts(pmCell *cell)
+{
+    //
+    // Set the parent to NULL in all cell->readouts before psFree(cell->readouts)
+    // in order to avoid memory reference counter problems.
+    //
+    psArray *readouts = cell->readouts;
+    for (psS32 i = 0 ; i < readouts->n ; i++) {
+        pmReadout *tmpReadout = readouts->data[i];
+        if (! tmpReadout) {
+            continue;
+        }
+        readouts->data[i] = NULL;
+        tmpReadout->parent = NULL;
+        psTrace(__func__, 9, "Will now free readout %lx...\n", (size_t)tmpReadout);
+        psFree(tmpReadout); // Drop the readout
+        #if PARENT_LINKS
+
+        psFree(cell); // Decrement reference counter on cell, since readout->parent isn't held any more
+        #endif
+
+    }
+    cell->readouts = psArrayRealloc(cell->readouts, 0);
+    cell->readouts->n = 0;
+}
+
+
+void pmChipFreeCells(pmChip *chip)
+{
+    //
+    // Set the parent to NULL in all chip->cells before psFree(chip->cells)
+    // in order to avoid memory reference counter problems.
+    //
+    psArray *cells = chip->cells;
+    for (int i = 0 ; i < cells->n ; i++) {
+        pmCell *tmpCell = cells->data[i];
+        if (! tmpCell) {
+            continue;
+        }
+        cells->data[i] = NULL;
+        tmpCell->parent = NULL;
+        pmCellFreeReadouts(tmpCell);// Drop all readouts the cell holds
+        psFree(tmpCell);            // Drop the cell
+        #if PARENT_LINKS
+
+        psFree(chip); // Decrement reference counter on chip, since cell->parent isn't held any more
+        #endif
+
+    }
+    chip->cells = psArrayRealloc(chip->cells, 0);
+    chip->cells->n = 0;
+}
+
+
+pmReadout *pmReadoutAlloc(pmCell *cell)
+{
+    pmReadout *tmpReadout = (pmReadout *) psAlloc(sizeof(pmReadout));
+    tmpReadout->row0 = 0;
+
+    tmpReadout->image = NULL;
+    tmpReadout->mask = NULL;
+    tmpReadout->weight = NULL;
+    tmpReadout->bias = psListAlloc(NULL);
+    tmpReadout->analysis = psMetadataAlloc();
+    #if PARENT_LINKS
+
+    tmpReadout->parent = psMemIncrRefCounter(cell);
+    #else
+
+    tmpReadout->parent = cell;
+    #endif
+
+    if (cell != NULL) {
+        cell->readouts = psArrayAdd(cell->readouts, 1, (psPtr) tmpReadout);
+    }
+
+    tmpReadout->process = true;            // All cells are processed by default
+    tmpReadout->file_exists = false;       // file not yet identified
+    tmpReadout->data_exists = false;       // data yet read in
+
+    psMemSetDeallocator(tmpReadout, (psFreeFunc) readoutFree);
+    return(tmpReadout);
+}
+
+// XXX: Verify these default values for row0, col0.
+// PAP: These values may disappear in the future in favour of values in the "concepts"?
+pmCell *pmCellAlloc(
+    pmChip *chip,
+    const char *name)
+{
+    pmCell *tmpCell = (pmCell *) psAlloc(sizeof(pmCell));
+
+    tmpCell->concepts = psMetadataAlloc();
+    psBool rc = psMetadataAddStr(tmpCell->concepts, PS_LIST_HEAD, "CELL.NAME", 0, NULL, name);
+    if (rc == false) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CELL.NAME to metadata.\n");
+    }
+    tmpCell->config = NULL;
+    tmpCell->analysis = psMetadataAlloc();
+    tmpCell->readouts = psArrayAlloc(0);
+    #if PARENT_LINKS
+
+    tmpCell->parent = psMemIncrRefCounter(chip);
+    #else
+
+    tmpCell->parent = chip;
+    #endif
+
+    if (chip != NULL) {
+        chip->cells = psArrayAdd(chip->cells, 1, (psPtr) tmpCell);
+    }
+    tmpCell->process = true;            // All cells are processed by default
+    tmpCell->file_exists = false;       // Not yet identified
+    tmpCell->data_exists = false;       // Not yet read in
+    tmpCell->hdu = NULL;
+
+    pmConceptsBlankCell(tmpCell);
+
+    psMemSetDeallocator(tmpCell, (psFreeFunc) cellFree);
+    return(tmpCell);
+}
+
+// XXX: Verify these default values for row0, col0.
+// PAP: row0, col0 may disappear in the future in favour of storing values in the "concepts".
+pmChip *pmChipAlloc(
+    pmFPA *fpa,
+    const char *name)
+{
+    pmChip *tmpChip = (pmChip *) psAlloc(sizeof(pmChip));
+
+    #if 0
+
+    tmpChip->col0 = 0;
+    tmpChip->row0 = 0;
+    tmpChip->toFPA = NULL;
+    tmpChip->fromFPA = NULL;
+    #endif
+
+    tmpChip->concepts = psMetadataAlloc();
+    psBool rc = psMetadataAddStr(tmpChip->concepts, PS_LIST_HEAD, "CHIP.NAME", 0, NULL, name);
+    if (rc == false) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not add CHIP.NAME to metadata.\n");
+    }
+    tmpChip->analysis = psMetadataAlloc();
+    tmpChip->cells = psArrayAlloc(0);
+    #if PARENT_LINKS
+
+    tmpChip->parent = psMemIncrRefCounter(fpa);
+    #else
+
+    tmpChip->parent = fpa;
+    #endif
+
+    if (fpa != NULL) {
+        fpa->chips = psArrayAdd(fpa->chips, 1, (psPtr) tmpChip);
+    }
+    tmpChip->process = true;            // Work on all chips, by default
+    tmpChip->file_exists = false;       // Not yet identified
+    tmpChip->data_exists = false;       // Not yet read in
+    tmpChip->hdu = NULL;
+
+    pmConceptsBlankChip(tmpChip);
+
+    psMemSetDeallocator(tmpChip, (psFreeFunc) chipFree);
+    return(tmpChip);
+}
+
+pmFPA *pmFPAAlloc(const psMetadata *camera)
+{
+    pmFPA *tmpFPA = (pmFPA *) psAlloc(sizeof(pmFPA));
+
+    #if 0
+
+    tmpFPA->fromTangentPlane = NULL;
+    tmpFPA->toTangentPlane = NULL;
+    tmpFPA->projection = NULL;
+    #endif
+
+    tmpFPA->concepts = psMetadataAlloc();
+    tmpFPA->analysis = NULL;
+    tmpFPA->camera = psMemIncrRefCounter((psPtr)camera);
+    tmpFPA->chips = psArrayAlloc(0);
+    tmpFPA->hdu = NULL;
+
+    pmConceptsBlankFPA(tmpFPA);
+
+    psMemSetDeallocator(tmpFPA, (psFreeFunc) FPAFree);
+    return(tmpFPA);
+}
+
+static psBool cellCheckParents(pmCell *cell)
+{
+    if (cell == NULL) {
+        return(true);
+    }
+    psBool flag = true;
+
+    for (psS32 i = 0 ; i < cell->readouts->n ; i++) {
+        pmReadout *tmpReadout = (pmReadout *) cell->readouts->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpReadout, false);
+        if (tmpReadout->parent != cell) {
+            tmpReadout->parent = cell;
+            flag = false;
+        }
+    }
+    return(flag);
+}
+
+static psBool chipCheckParents(pmChip *chip)
+{
+    if (chip == NULL) {
+        return(true);
+    }
+    psBool flag = true;
+
+    for (psS32 i = 0 ; i < chip->cells->n ; i++) {
+        pmCell *tmpCell = (pmCell *) chip->cells->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpCell, false);
+        if (tmpCell->parent != chip) {
+            tmpCell->parent = chip;
+            flag = false;
+        }
+
+        flag &= cellCheckParents(tmpCell);
+    }
+    return(flag);
+}
+
+psBool pmFPACheckParents(pmFPA *fpa)
+{
+    if (fpa == NULL) {
+        return(true);
+    }
+    psBool flag = true;
+
+    for (psS32 i = 0 ; i < fpa->chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) fpa->chips->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpChip, false);
+        if (tmpChip->parent != fpa) {
+            tmpChip->parent = fpa;
+            flag = false;
+        }
+
+        flag &= chipCheckParents(tmpChip);
+    }
+    return(flag);
+}
+
+/** functions to turn on/off the file_exists flag **/
+bool pmFPASetFileStatus (pmFPA *fpa, bool status)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        pmChipSetFileStatus (chip, status);
+    }
+    return true;
+}
+
+bool pmChipSetFileStatus (pmChip *chip, bool status)
+{
+
+    chip->file_exists = status;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        pmCellSetFileStatus (cell, status);
+    }
+    return true;
+}
+
+bool pmCellSetFileStatus (pmCell *cell, bool status)
+{
+
+    cell->file_exists = status;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->file_exists = status;
+    }
+    return true;
+}
+
+/** functions to turn on/off the data_exists flag **/
+bool pmFPASetDataStatus (pmFPA *fpa, bool status)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        pmChipSetDataStatus (chip, status);
+    }
+    return true;
+}
+
+bool pmChipSetDataStatus (pmChip *chip, bool status)
+{
+
+    chip->data_exists = status;
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        pmCellSetDataStatus (cell, status);
+    }
+    return true;
+}
+
+bool pmCellSetDataStatus (pmCell *cell, bool status)
+{
+
+    cell->data_exists = status;
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        readout->data_exists = status;
+    }
+    return true;
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+
+// Set cells within a chip to be processed or not
+static bool setCellsProcess(const pmChip *chip, // Chip of interest
+                            bool process  // Process this chip?
+                           )
+{
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+
+    psArray *cells = chip->cells;       // Component cells
+    if (! cells) {
+        return false;
+    }
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *tmpCell = cells->data[i]; // Cell of interest
+        if (tmpCell) {
+            tmpCell->process = process;
+        }
+    }
+
+    return true;
+}
+
+/*****************************************************************************
+XXX EAM : I've added the 'exclusive' option.  if true all other chips are de-selected
+XXX EAM : a negative value is valid and, in combinations with exclusive, de-selects all chips
+ *****************************************************************************/
+bool pmFPASelectChip(pmFPA *fpa, int chipNum, bool exclusive)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if ((chips == NULL) || (chipNum >= chips->n)) {
+        return(false);
+    }
+
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i];
+        if (tmpChip == NULL) {
+            continue;
+        }
+        if (i == chipNum) {
+            tmpChip->process = true;
+            setCellsProcess(tmpChip, true);
+        } else {
+            if (exclusive) {
+                tmpChip->process = false;
+                setCellsProcess(tmpChip, false);
+            }
+        }
+
+    }
+
+    return true;
+}
+
+/*****************************************************************************
+XXX EAM : I've added the 'exclusive' option.  if true all other chips are de-selected
+XXX EAM : a negative value is valid and, in combinations with exclusive, de-selects all cells
+XXX this function should probably be re-defined to merge with 'setCellsProcess'
+ *****************************************************************************/
+bool pmChipSelectCell(pmChip *chip, int cellNum, bool exclusive)
+{
+    assert(chip);
+
+    psArray *cells = chip->cells;       // Component cells
+    if (!cells || cellNum > cells->n) {
+        return false;
+    }
+
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];
+        if (!cell) {
+            continue;
+        }
+        if (i == cellNum) {
+            cell->process = true;
+        } else {
+            if (exclusive) {
+                cell->process = false;
+            }
+        }
+    }
+    return true;
+}
+
+/*****************************************************************************
+XXX: The SDRS is ambiguous on a few things:
+    Whether or not the other chips should be set process=true. [PAP: No]
+    Should we return the number of chip process=true before or after they're set, [PAP: After]
+ *****************************************************************************/
+/**
+ *
+ * pmFPAExcludeChip shall set process to false only for the specified chip
+ * number (chipNum). In the event that the specified chip number does not exist
+ * within the fpa, the function shall generate a warning, and perform no action.
+ * The function shall return the number of chips within the fpa that have process
+ * set to true.
+ *
+ */
+int pmFPAExcludeChip(
+    pmFPA *fpa,
+    int chipNum)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+
+    psArray *chips = fpa->chips;        // Component chips
+    if (chips == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: fpa->chips == NULL\n");
+        return(0);
+    }
+    if ((chipNum >= chips->n) || (NULL == (pmChip *) chips->data[chipNum])) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: the specified chip (%d) does not exist.\n", chipNum);
+        return(0);
+    }
+
+    int numChips = 0;                   // Number of chips to be processed
+    for (int i = 0 ; i < chips->n ; i++) {
+        pmChip *tmpChip = (pmChip *) chips->data[i]; // Chip of interest
+        if (tmpChip != NULL) {
+            if (i == chipNum) {
+                tmpChip->process = false;
+                setCellsProcess(tmpChip, false); // Wipe out the cell as well
+            } else if (tmpChip->process) {
+                numChips++;
+            }
+        }
+    }
+
+    return(numChips);
+}
+
+int pmChipExcludeCell(pmChip *chip,
+                      int cellNum
+                     )
+{
+    assert(chip);
+
+    psArray *cells = chip->cells;       // The component cells
+    if (!cells || cellNum > cells->n) {
+        return 0;
+    }
+
+    int numCells = 0;                   // Number of cells to be processed
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];
+        if (!cell) {
+            continue;
+        }
+        if (i == cellNum) {
+            cell->process = false;
+        } else {
+            numCells++;
+        }
+    }
+
+    return numCells;
+}
+
+
Index: trunk/psModules/src/astrom/pmFPA.h
===================================================================
--- trunk/psModules/src/astrom/pmFPA.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPA.h	(revision 6872)
@@ -0,0 +1,279 @@
+/** @file  pmFPA.h
+*
+*  @brief This file defines the basic types the focal plane hierarchy.
+*
+*  @ingroup AstroImage
+*
+*  @author GLG, MHPCC
+*
+*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+#ifndef PM_FPA_H
+#define PM_FPA_H
+#if HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "pslib.h"
+#include "pmHDU.h"
+
+/// @addtogroup AstroImage
+/// @{
+
+/** Focal plane data structure
+ *
+ *  A focal plane consists of one or more chips (according to the number of
+ *  pieces of contiguous silicon). It contains metadata containers for the
+ *  concepts and analysis, a link to the parent, and pointers to the FITS header,
+ *  if that corresponds to this level (the FPA may be the PHU, but will not ever
+ *  contain pixels). For astrometry, it contains a transformation from the focal
+ *  plane to the tangent plane and the fixed pattern residuals. It is expected
+ *  that the transformation will consist of two 4D polynomials (i.e. a function
+ *  of two coordinates in position, the magnitude of the object, and the color of
+ *  the object) in order to correct for optical distortions and the effects of
+ *  the atmosphere; hence we think that it is prudent to include a reverse
+ *  transformation which will be derived from numerically inverting the forward
+ *  transformation.
+ *
+ */
+typedef struct
+{
+    #if 0
+    // 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
+    #endif
+    // Information
+    psMetadata *concepts;               ///< Cache for PS concepts
+    psMetadata *analysis;               ///< FPA-level analysis metadata
+    const psMetadata *camera;           ///< Camera configuration
+    psArray *chips;                     ///< The chips
+    pmHDU *hdu;                         ///< FITS data
+}
+pmFPA;
+
+/** Chip data structure
+ *
+ *  A chip consists of one or more cells (according to the number of amplifiers
+ *  on the device). The chip contains metadata containers for the concepts and
+ *  analysis, a link to the parent, and pointers to the pointers to the various
+ *  FITS data, if that corresponds to this level. For astrometry, in addition to
+ *  the rough positioning information, it contains a coordinate transform from
+ *  the chip to the focal plane. It is expected that this transform will consist
+ *  of two second-order 2D polynomials; hence we think that it is prudent to
+ *  include a reverse transformation which will be derived from numerically
+ *  inverting the forward transformation. A boolean indicates whether the chip is
+ *  of interest, allowing it to be excluded from analysis.
+ *
+ */
+typedef struct
+{
+    #if 0
+    // 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
+    #endif
+    // 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 process;                       ///< Do we bother about reading and working with this chip?
+    bool file_exists;                   ///< Does the file for this chip exist (read case only)?
+    bool data_exists;                   ///< Does the data for this chip exist (read case only)?
+    pmHDU *hdu;                         ///< FITS data
+}
+pmChip;
+
+/** Cell data structure
+ *
+ *  A cell consists of one or more readouts.  It also contains a pointer to the
+ *  cell's metadata, and its parent chip.  On the astrometry side, it also
+ *  contains coordinate transforms from the cell to chip, from the cell to
+ *  focal-plane, as well as a "quick and dirty" tranform from the cell to
+ *  sky coordinates.
+ *
+ */
+typedef struct
+{
+    psMetadata *concepts;               ///< Cache for PS concepts
+    psMetadata *config;                 ///< Cell configuration info
+    psMetadata *analysis;               ///< Cell-level analysis metadata
+    psArray *readouts;                  ///< The readouts (referred to by number)
+    pmChip *parent;                     ///< Parent chip
+    bool process;                       ///< Do we bother about reading and working with this cell?
+    bool file_exists;                   ///< Does the file for this cell exist (read case only)?
+    bool data_exists;                   ///< Does the data for this cell exist (read case only)?
+    pmHDU *hdu;                         ///< FITS data
+}
+pmCell;
+
+/** Readout data structure.
+ *
+ *  A readout is the result of a single read of a cell (or a portion thereof).
+ *  It contains the offset from the lower-left corner of the chip, in the case
+ *  that the CCD was windowed, as well as the binning factors and parity (if the
+ *  binning value is negative, then the parity is reversed). It also contains the
+ *  pixel data, metadata containers for the concepts and analysis, and a link to
+ *  the parent.
+ *
+ */
+typedef struct
+{
+    int row0;                           ///< Row offset; non-zero if reading in bit by bit
+    psImage *image;                     ///< Imaging area of readout
+    psImage *mask;                      ///< Mask of input image
+    psImage *weight;                    ///< Weight of input image
+    psList *bias;                       ///< Overscan images
+    psMetadata *analysis;               ///< Readout-level analysis metadata
+    pmCell *parent;                     ///< Parent cell
+    bool process;                       ///< Do we bother about reading and working with this readout?
+    bool file_exists;                   ///< Does the file for this readout exist (read case only)?
+    bool data_exists;                   ///< Does the data for this readout exist (read case only)?
+}
+pmReadout;
+
+void pmCellFreeReadouts(pmCell *cell);
+void pmChipFreeCells(pmChip *chip);
+
+/** Allocates a pmReadout
+ *
+ *  The constructor shall make an empty pmReadout. If the parent cell is not
+ *  NULL, the parent link is made and the readout shall be placed in the
+ *  parents array of readouts. The metadata containers shall be allocated. All
+ *  other pointers in the structure shall be initialized to NULL.
+ *
+ *  @return pmReadout*    newly allocated pmReadout with all internal pointers set to NULL
+ */
+pmReadout *pmReadoutAlloc(
+    pmCell *cell                        ///< Parent cell
+);
+
+/** Allocates a pmCell
+ *
+ *  The constructor shall make an empty pmCell. If the parent chip is not NULL,
+ *  the parent link is made and the cell shall be placed in the parents array of
+ *  cells. The readouts array shall be allocated with a zero size, and the
+ *  metadata containers constructed. All other pointers in the structure shall be
+ *  initialized to NULL.
+ *
+ *  @return pmCell*    newly allocated pmCell
+ */
+pmCell *pmCellAlloc(
+    pmChip *chip,       ///< Parent chip
+    const char *name    ///< Name of cell
+);
+
+/** Allocates a pmChip
+ *
+ *  The constructor shall make an empty pmChip. If the parent fpa is not NULL,
+ *  the parent link is made and the chip shall be placed in the parent's array
+ *  of chips. The cells array shall be allocated with a zero size, and the
+ *  metadata containers constructed. All other pointers in the structure shall be
+ *  initialized to NULL.
+ *
+ *  @return pmChip*    newly allocated pmChip
+ */
+pmChip *pmChipAlloc(
+    pmFPA *fpa,                         ///< FPA to which the chip belongs
+    const char *name                    ///< Name of chip
+);
+
+/** Allocates a pmFPA
+ *
+ *  The constructor shall make an empty pmFPA. The chips array shall be
+ *  allocated with a zero size, the camera and db pointers set to the values
+ *  provided, and the concepts metadata constructed. All other pointers in the
+ *  structure shall be initialized to NULL.
+ *
+ */
+pmFPA *pmFPAAlloc(
+    const psMetadata *camera            ///< Camera configuration
+);
+
+
+/** Verify parent links.
+ *
+ *  This function checks the validity of the parent links in the FPA hierarchy.
+ *  If a parent link is not set (or not set correctly), it is corrected, and the
+ *  function shall return false. If all the parent pointers were correct, the
+ *  function shall return true.
+ *
+ */
+bool pmFPACheckParents(
+    pmFPA *fpa
+);
+
+/* functions to turn on/off the file_exists flags */
+bool pmFPASetFileStatus (pmFPA *fpa, bool status);
+bool pmChipSetFileStatus (pmChip *chip, bool status);
+bool pmCellSetFileStatus (pmCell *cell, bool status);
+
+/* functions to turn on/off the data_exists flags */
+bool pmFPASetDataStatus (pmFPA *fpa, bool status);
+bool pmChipSetDataStatus (pmChip *chip, bool status);
+bool pmCellSetDataStatus (pmCell *cell, bool status);
+
+/** Specify the level for an operation.
+ */
+typedef enum {
+    PM_FPA_LEVEL_NONE,                  ///< No particular level specified
+    PM_FPA_LEVEL_FPA,                   ///< Level corresponds to an FPA
+    PM_FPA_LEVEL_CHIP,                  ///< Level corresponds to a Chip
+    PM_FPA_LEVEL_CELL,                  ///< Level corresponds to a Cell
+    PM_FPA_LEVEL_READOUT                ///< Level corresponds to a Readout
+} pmFPALevel;
+
+
+/**
+ *
+ * pmFPASelectChip shall set valid to true for the specified chip number
+ * (chipNum), and all other chips shall have valid set to false. In the event
+ * that the specified chip number does not exist within the fpa, the function
+ * shall return false.
+ *
+ */
+bool pmFPASelectChip(
+    pmFPA *fpa,
+    int chipNum,
+    bool exclusive
+);
+
+bool pmChipSelectCell(pmChip *chip,
+                      int cellNum,
+                      bool exclusive
+                     );
+
+/**
+ *
+ * pmFPAExcludeChip shall set valid to false only for the specified chip
+ * number (chipNum). In the event that the specified chip number does not exist
+ * within the fpa, the function shall generate a warning, and perform no action.
+ * The function shall return the number of chips within the fpa that have valid
+ * set to true.
+ *
+ */
+int pmFPAExcludeChip(
+    pmFPA *fpa,
+    int chipNum
+);
+
+int pmChipExcludeCell(pmChip *chip,
+                      int cellNum
+                     );
+
+// Set the weights and masks within a cell, based on the gain and RN
+bool pmCellSetWeights(pmCell *cell // Cell for which to set weights
+                     );
+
+
+
+#endif // #ifndef PM_FPA_H
Index: trunk/psModules/src/astrom/pmFPAAstrometry.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAAstrometry.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAAstrometry.c	(revision 6872)
@@ -0,0 +1,512 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPAAstrometry.h"
+#include "pmFPA.h"
+
+
+/*****************************************************************************
+checkValidImageCoords(): this is a private function which simply determines if
+the supplied x,y coordinates are in the range for the supplied psImage.
+ 
+XXX: What about col0 and row0
+XXX: This should return a psBool.
+XXX: Macro this for speed.
+ *****************************************************************************/
+static psS32 checkValidImageCoords(
+    double x,
+    double y,
+    psImage* tmpImage)
+{
+    PS_ASSERT_IMAGE_NON_NULL(tmpImage, 0);
+
+    // The FLT_EPSILON is because -0.0 was failing this.
+    if (((x+FLT_EPSILON) < 0.0) || (x > (double)tmpImage->numCols) ||
+            ((y+FLT_EPSILON) < 0.0) || (y > (double)tmpImage->numRows)) {
+        return (0);
+    }
+
+    return (1);
+}
+
+/*****************************************************************************/
+/* FUNCTION IMPLEMENTATION - PUBLIC                                          */
+/*****************************************************************************/
+
+pmCell* pmCellInFPA(
+    const psPlane* fpaCoord,
+    const pmFPA* FPA)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
+
+    pmChip* tmpChip = NULL;
+    psPlane chipCoord;
+    pmCell* outCell = NULL;
+
+    // Determine which chip contains the fpaCoords.
+    tmpChip = pmChipInFPA(fpaCoord, FPA);
+    if (tmpChip == NULL) {
+        return(NULL);
+    }
+
+    // Convert to those chip coordinates.
+    psPlane *rc = pmCoordFPAToChip(&chipCoord, fpaCoord, tmpChip);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine Chip coords.\n");
+        return(NULL);
+    }
+
+    // Determine which cell contains those chip coordinates.
+    outCell = pmCellInChip(&chipCoord, tmpChip);
+    if (outCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
+        return(NULL);
+    }
+
+    return (outCell);
+}
+
+pmChip* pmChipInFPA(
+    const psPlane* fpaCoord,
+    const pmFPA* FPA)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(FPA->chips, NULL);
+
+    psArray* chips = FPA->chips;
+    psS32 nChips = chips->n;
+    psPlane chipCoord;
+    pmCell *tmpCell = NULL;
+
+    //
+    // Loop through every chip in this FPA.  Convert the original FPA
+    // coordinates to chip coordinates for that chip.  Then, determine if any
+    // cells in that chip contain those chip coordinates.
+    // XXX: Depending on the number of chips, and their topology, there may be
+    // a much more efficient way of doing this.
+    //
+    for (psS32 i = 0; i < nChips; i++) {
+        pmChip* tmpChip = chips->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpChip, NULL);
+        PS_ASSERT_PTR_NON_NULL(tmpChip->fromFPA, NULL);
+
+        psPlaneTransformApply(&chipCoord, tmpChip->fromFPA, fpaCoord);
+
+        tmpCell = pmCellInChip(&chipCoord, tmpChip);
+        if (tmpCell != NULL) {
+            return(tmpChip);
+        }
+    }
+
+    psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the chip.\n");
+    return (NULL);
+}
+
+
+pmCell* pmCellInChip(
+    const psPlane* chipCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psPlane cellCoord;
+    psArray* cells;
+
+    cells = chip->cells;
+    if (cells == NULL) {
+        return NULL;
+    }
+
+    //
+    // We loop over each cell in the chip.  We transform the chipCoord into
+    // a cellCoord for that cell and determine if that cellCoord is valid.
+    // If so, then we return that cell.
+    // XXX: Depending on the number of cells, and their topology, there may be
+    // a much more efficient way of doing this.
+    //
+    for (psS32 i = 0; i < cells->n; i++) {
+        pmCell* tmpCell = (pmCell* ) cells->data[i];
+        PS_ASSERT_PTR_NON_NULL(tmpCell, NULL);
+
+        psPlaneTransform *chipToCell = NULL;
+        if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
+            chipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
+        } else {
+            psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
+            //chipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
+            chipToCell = NULL;
+        }
+        if (chipToCell == NULL) {
+            psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
+            return(NULL);
+        }
+        psArray* readouts = tmpCell->readouts;
+
+        if (readouts != NULL) {
+            for (psS32 j = 0; j < readouts->n; j++) {
+                pmReadout* tmpReadout = readouts->data[j];
+                PS_ASSERT_READOUT_NON_NULL(tmpReadout, NULL);
+
+                psPlaneTransformApply(&cellCoord,
+                                      chipToCell,
+                                      chipCoord);
+
+                if (checkValidImageCoords(cellCoord.x,
+                                          cellCoord.y,
+                                          tmpReadout->image)) {
+                    psFree(chipToCell);
+                    return (tmpCell);
+                }
+            }
+        }
+        psFree(chipToCell);
+    }
+
+    //psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the cell.\n");
+    return (NULL);
+}
+
+
+psPlane* pmCoordCellToFPA(
+    psPlane* fpaCoord,
+    const psPlane* cellCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psPlane *rc = psPlaneTransformApply(fpaCoord, cell->toFPA, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+psPlane* pmCoordChipToFPA(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    psPlane *rc = psPlaneTransformApply(outCoord, chip->toFPA, inCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform chip coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+psPlane* pmCoordFPAToChip(
+    psPlane* chipCoord,
+    const psPlane* fpaCoord,
+    const pmChip* chip)
+{
+    PS_ASSERT_PTR_NON_NULL(fpaCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip->fromFPA, NULL);
+
+    psPlane *rc = psPlaneTransformApply(chipCoord, chip->fromFPA, fpaCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to Chip coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordCellToChip(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+
+    psPlane *rc = psPlaneTransformApply(outCoord, cell->toChip, inCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Cell coords to Chip coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordChipToCell(
+    psPlane* cellCoord,
+    const psPlane* chipCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(chipCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+
+    pmCell *tmpCell = pmCellInChip(chipCoord, cell->parent);
+    if (tmpCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine the proper cell.\n");
+        return(NULL);
+    }
+
+    psPlaneTransform *tmpChipToCell = NULL;
+    PS_ASSERT_PTR_NON_NULL(tmpCell->toChip, NULL);
+    if (true ==  p_psIsProjectionLinear(tmpCell->toChip)) {
+        tmpChipToCell = p_psPlaneTransformLinearInvert(tmpCell->toChip);
+    } else {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: non-linear cell->chip transforms are not yet implemented.\n");
+        // XXX: tmpChipToCell = psPlaneTransformInvert(NULL, tmpCell->toChip, NULL, -1);
+        tmpChipToCell = NULL;
+    }
+    if (tmpChipToCell == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not invert the Cell->toChip transform.\n");
+        return(NULL);
+    }
+
+    psPlane *rc = psPlaneTransformApply(cellCoord, tmpChipToCell, chipCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform Chip coords to Cell coords.\n");
+    }
+    psFree(tmpChipToCell);
+    return(rc);
+}
+
+psPlane* pmCoordFPAToTP(
+    psPlane* outCoord,
+    const psPlane* inCoord,
+    double color,
+    double magnitude,
+    const pmFPA* fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(inCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psPlane *rc = psPlaneDistortApply(outCoord, fpa->toTangentPlane, inCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform FPA coords to tangent plane coords.\n");
+    }
+    return(rc);
+}
+
+psPlane* pmCoordTPToFPA(
+    psPlane* fpaCoord,
+    const psPlane* tpCoord,
+    double color,
+    double magnitude,
+    const pmFPA* fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->fromTangentPlane, NULL);
+
+    psPlane *rc = psPlaneDistortApply(fpaCoord, fpa->fromTangentPlane, tpCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to FPA coords.\n");
+    }
+    return(rc);
+}
+
+
+/*****************************************************************************
+XXXDeproject(outSphere, coord, projection): This private routine is a wrapper
+for p_psDeproject().  The reason: p_psDeproject() and p_psProject() combined
+do not seem to produce the original coordinates when they even though they
+should.  XXXDeproject() simply negates the ->r and ->d members of the output
+psSphere if the input ->y is larger than 0.0.  I don't know why it works.
+ 
+I'm guessing the p_psProject() and p_psDeproject() functions have bugs.
+ 
+XXX: It appears that p_psProject() and p_psDeproject() have been fixed.
+Remove this.
+ *****************************************************************************/
+psSphere* XXXDeproject(
+    psSphere *outSphere,
+    const psPlane* coord,
+    const psProjection* projection)
+{
+    psSphere *rc = p_psDeproject(outSphere, coord, projection);
+
+    if (coord->y >= 0.0) {
+        rc->d = -rc->d;
+        rc->r = -rc->r;
+    }
+
+    return(rc);
+}
+
+/*****************************************************************************
+  *****************************************************************************/
+psSphere* pmCoordTPToSky(
+    psSphere* outSphere,
+    const psPlane* tpCoord,
+    const psProjection *projection)
+{
+    PS_ASSERT_PTR_NON_NULL(tpCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(projection, NULL);
+
+    //    psSphere *rc = XXXDeproject(outSphere, tpCoord, projection);
+    psSphere *rc = p_psDeproject(outSphere, tpCoord, projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform tangent plane coords to sky coords.\n");
+    }
+    return(rc);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToTP(
+    psPlane* tpCoord,
+    const psSphere* in,
+    const psProjection *projection)
+{
+    PS_ASSERT_PTR_NON_NULL(in, NULL);
+    PS_ASSERT_PTR_NON_NULL(projection, NULL);
+
+    psPlane *rc = p_psProject(tpCoord, in, projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to tangent plane coords.\n");
+    }
+    return(rc);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psSphere* pmCoordCellToSky(
+    psSphere* skyCoord,
+    const psPlane* cellCoord,
+    double color,
+    double magnitude,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->toTangentPlane, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent->projection, NULL);
+    psPlane fpaCoord;
+    psPlane tpCoord;
+    psPlane *rc;
+    pmFPA* parFPA = (cell->parent)->parent;
+
+    // Convert the input cell coordinates to FPA coordinates.
+    rc = psPlaneTransformApply(&fpaCoord, cell->toFPA, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to FPA coords.\n");
+        return(NULL);
+    }
+
+    // Convert the FPA coordinates to tangent plane Coordinates.
+    rc = psPlaneDistortApply(&tpCoord, parFPA->toTangentPlane, &fpaCoord, color, magnitude);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform FPA coords to tangent plane coords.\n");
+        return(NULL);
+    }
+
+    // Convert the tangent plane Coordinates to sky coordinates.
+    psSphere *rc2 = pmCoordTPToSky(skyCoord, &tpCoord, parFPA->projection);
+    if (rc2 == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform cell coords to sky coords.\n");
+    }
+
+    return(rc2);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToCell(
+    psPlane* cellCoord,
+    const psSphere* skyCoord,
+    float color,
+    float magnitude,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->parent->parent, NULL);
+    pmChip *parChip = cell->parent;
+    pmFPA *parFPA = parChip->parent;
+    psPlane tpCoord;
+    psPlane fpaCoord;
+    psPlane chipCoord;
+    psPlane *rc;
+
+    // Convert the skyCoords to tangent plane coords.
+    rc = pmCoordSkyToTP(&tpCoord, skyCoord, parFPA->projection);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine tangent plane coords.\n");
+        return(NULL);
+    }
+
+    // Convert the tangent plane coords to FPA coords.
+    rc = pmCoordTPToFPA(&fpaCoord, &tpCoord, color, magnitude, parFPA);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine FPA coords.\n");
+        return(NULL);
+    }
+
+    // Convert the FPA coords to chip coords.
+    rc = pmCoordFPAToChip(&chipCoord, &fpaCoord, parChip);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine chip coords.\n");
+        return(NULL);
+    }
+
+    // Convert the chip coords to cell coords.
+    rc = pmCoordChipToCell(cellCoord, &chipCoord, cell);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not determine cell coords.\n");
+        return(NULL);
+    }
+
+    return (cellCoord);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psSphere* pmCoordCellToSkyQuick(
+    psSphere* outSphere,
+    const psPlane* cellCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(cellCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
+    psPlane outPlane;
+    psPlane *rc;
+    rc = psPlaneTransformApply(&outPlane, cell->toSky, cellCoord);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could transform cell coords to sky coords.\n");
+        return(NULL);
+    }
+
+    psSphere *out = outSphere;
+    if (out == NULL) {
+        out = psSphereAlloc();
+    }
+    out->r = outPlane.y;
+    out->d = outPlane.x;
+
+    return(out);
+}
+
+/*****************************************************************************
+ *****************************************************************************/
+psPlane* pmCoordSkyToCellQuick(
+    psPlane* cellCoord,
+    const psSphere* skyCoord,
+    const pmCell* cell)
+{
+    PS_ASSERT_PTR_NON_NULL(skyCoord, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell, NULL);
+    PS_ASSERT_PTR_NON_NULL(cell->toSky, NULL);
+    psPlane skyPlane;
+    skyPlane.y = skyCoord->r;
+    skyPlane.x = skyCoord->d;
+
+    psPlane *rc = psPlaneTransformApply(cellCoord, cell->toSky, &skyPlane);
+    if (rc == NULL) {
+        psLogMsg(__func__, PS_LOG_WARN, "WARNING: could not transform sky to cell coords.\n");
+    }
+    return(cellCoord);
+}
Index: trunk/psModules/src/astrom/pmFPAAstrometry.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAAstrometry.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAAstrometry.h	(revision 6872)
@@ -0,0 +1,188 @@
+#ifndef PM_FPA_ASTROMETRY_H
+#define PM_FPA_ASTROMETRY_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+/** Find cooresponding cell for given FPA coordinate
+ *
+ *  @return pmCell*    the cell cooresponding to the coord in FPA
+ */
+pmCell* pmCellInFPA(
+    const psPlane* coord,              ///< the coordinate in FPA plane
+    const pmFPA* FPA                   ///< the FPA to search for the cell
+);
+
+
+/** Find cooresponding chip for given FPA coordinate
+ *
+ *  @return pmChip*    the chip cooresponding to coord
+ */
+pmChip* pmChipInFPA(
+    const psPlane* coord,              ///< the coordinate in FPA plane
+    const pmFPA* FPA                   ///< the FPA to search for the cell
+);
+
+
+/** Find cooresponding cell for given Chip coordinate
+ *
+ *  @return pmCell*    the cell cooresponding to coord
+ */
+pmCell* pmCellInChip(
+    const psPlane* coord,              ///< the coordinate in Chip plane
+    const pmChip* chip                 ///< the chip to search for the cell
+);
+
+
+/** Translate a cell coordinate into a chip coordinate
+ *
+ *  @return psPlane*    the resulting chip coordinate
+ */
+psPlane* pmCoordCellToChip(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within Cell
+    const pmCell* cell                 ///< the Cell in interest
+);
+
+
+/** Translate a chip coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordChipToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within Chip
+    const pmChip* chip                 ///< the chip in interest
+);
+
+
+/** Translate a FPA coordinate into a Tangent Plane coordinate
+ *
+ *  @return psPlane*    the resulting Tangent Plane coordinate
+ */
+psPlane* pmCoordFPAToTP(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within FPA
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmFPA* fpa                   ///< the FPA in interest
+);
+
+
+/** Translate a Tangent Plane coordinate into a Sky coordinate
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordTPToSky(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                ///< the coordinate within Tangent Plane
+    const psProjection *projection
+);
+
+/** Translate a cell coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordCellToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a cell coordinate into a Sky coordinate
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordCellToSky(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a cell coordinate into a Sky coordinate using a 'quick and
+ *  dirty' method
+ *
+ *  @return psSphere*    the resulting Sky coordinate
+ */
+psSphere* pmCoordCellToSkyQuick(
+    psSphere* out,                     ///< a sphere struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within cell
+    const pmCell* cell                 ///< the cell in interest
+);
+
+
+/** Translate a Sky coordinate into a Tangent Plane coordinate
+ *
+ *  @return psPlane*    the resulting Tangent Plane coordinate
+ */
+psPlane* pmCoordSkyToTP(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the sky coordinate
+    const psProjection *projection
+);
+
+/** Translate a Tangent Plane coordinate into a FPA coordinate
+ *
+ *  @return psPlane*    the resulting FPA coordinate
+ */
+psPlane* pmCoordTPToFPA(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the coordinate within tangent plane
+    double color,                      ///< Color of source
+    double magnitude,                  ///< Magnitude of source
+    const pmFPA* fpa                   ///< the FPA of interest
+);
+
+
+/** Translate a FPA coordinate into a chip coordinate
+ *
+ *  @return psPlane*    the resulting chip coordinate
+ */
+psPlane* pmCoordFPAToChip(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the FPA coordinate
+    const pmChip* chip                 ///< the chip of interest
+);
+
+
+/** Translate a chip coordinate into a cell coordinate
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordChipToCell(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psPlane* in,                 ///< the Chip coordinate
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+/** Translate a sky coordinate into a cell coordinate
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordSkyToCell(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the Sky coordinate
+    float color,                       ///< Color of source
+    float magnitude,                   ///< Magnitude of source
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+/** Translate a sky coordinate into a cell coordinate using a 'quick and
+ *  dirty' method
+ *
+ *  @return psPlane*    the resulting cell coordinate
+ */
+psPlane* pmCoordSkyToCellQuick(
+    psPlane* out,                      ///< a plane struct to recycle. If NULL, a new struct is created
+    const psSphere* in,                ///< the Sky coordinate
+    const pmCell* cell                 ///< the cell of interest
+);
+
+
+#endif
Index: trunk/psModules/src/astrom/pmFPAConceptsGet.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAConceptsGet.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConceptsGet.c	(revision 6872)
@@ -0,0 +1,1197 @@
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include "pslib.h"
+
+#include "pmAstrometry.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
+        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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
+        //        const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+        // columns
+        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+        // columns
+
+        // Now, need to get the "given"s
+        if (strlen(givenCols) || strlen(givenPS)) {
+            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+            psList *values = psStringSplit(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");
+    return NULL;
+}
+
+
+// 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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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.
+    return true;
+}
+
+
+// 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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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 = psStringSplit(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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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 (strcasecmp(timeFormat, "JD") == 0) {
+                                double timeValue = strtod (timeString, NULL);
+                                time = psTimeFromJD(timeValue);
+                            } else if (strcasecmp(timeFormat, "MJD") == 0) {
+                                double timeValue = strtod (timeString, NULL);
+                                time = psTimeFromMJD(timeValue);
+                            } 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 = psMetadataLookupStr(&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");
+
+    return true;
+}
+
+
+// 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.
+}
+
+
+// Set up concepts for blank image
+bool pmFPABlankConcepts(pmFPA *fpa      // The FPA
+                       )
+{
+    assert(fpa);
+
+    // FPA-level concepts
+    if (! fpa->concepts) {
+        fpa->concepts = psMetadataAlloc();
+    }
+    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.NAME", PS_META_REPLACE, "Name of FPA", "");
+    psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.AIRMASS", PS_META_REPLACE,
+                     "Airmass at boresight", NAN);
+    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.FILTER", PS_META_REPLACE, "Filter used", "");
+    psMetadataAddF32(fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE,
+                     "Position angle for instrument", NAN);
+    psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "FPA.RADECSYS", PS_META_REPLACE,
+                     "Celestial coordinate system", "");
+    psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.RA", PS_META_REPLACE,
+                     "Right Ascension of the boresight (radians)", NAN);
+    psMetadataAddF64(fpa->concepts, PS_LIST_TAIL, "FPA.DEC", PS_META_REPLACE,
+                     "Declination of the boresight (radians)", NAN);
+
+    // Iterate through the chips
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The chip of interest
+        if (! chip) {
+            continue;
+        }
+
+        // Chip-level concepts
+        if (! chip->concepts) {
+            chip->concepts = psMetadataAlloc();
+        }
+        // CHIP.NAME *should* have been added by pmFPAConstruct --- make sure
+        psMetadataItem *chipNameItem = psMetadataLookup(chip->concepts, "CHIP.NAME");
+        if (! chipNameItem) {
+            psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "CHIP.NAME", PS_META_REPLACE, "Name of Chip", "");
+        }
+
+        // Iterate through the cells
+        psArray *cells = chip->cells;   // Array of cells
+        for (int j = 0; j < cells->n; j++) {
+            pmCell *cell = cells->data[i]; // The cell of interest
+            if (! cell) {
+                continue;
+            }
+
+            // Cell-level concepts
+            if (! cell->concepts) {
+                cell->concepts = psMetadataAlloc();
+            }
+            // CELL.NAME *should* have been added by pmFPAConstruct --- make sure
+            psMetadataItem *cellNameItem = psMetadataLookup(cell->concepts, "CELL.NAME");
+            if (!cellNameItem) {
+                psMetadataAddStr(fpa->concepts, PS_LIST_TAIL, "CELL.NAME", PS_META_REPLACE,
+                                 "Name of Cell", "");
+            }
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.GAIN", PS_META_REPLACE, "CCD gain (e/count)", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.READNOISE", PS_META_REPLACE, "CCD read noise (e)", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.SATURATION", PS_META_REPLACE, "Saturation level (ADU)", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.BAD", PS_META_REPLACE, "Bad level (ADU)", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.XPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.YPARITY", PS_META_REPLACE, "Orientation in x compared to the rest of the FPA", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.READDIR", PS_META_REPLACE, "Read direction: 1=row, 2=col", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.EXPOSURE", PS_META_REPLACE, "Exposure time (sec)", NAN);
+            psMetadataAddF32(cell->concepts, PS_LIST_TAIL, "CELL.DARKTIME", PS_META_REPLACE, "Time since CCD flush (sec)", NAN);
+            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.XBIN", PS_META_REPLACE, "Binning in x", 0);
+            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.YBIN", PS_META_REPLACE, "Binning in y", 0);
+            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.X0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);
+            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.Y0", PS_META_REPLACE, "Position of (0,0) on the chip", 0);
+
+            psRegion *trimsec = psAlloc(sizeof(psRegion)); // Dummy region
+            trimsec->x0 = NAN;
+            trimsec->x1 = NAN;
+            trimsec->y0 = NAN;
+            trimsec->y1 = NAN;
+            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.TRIMSEC", PS_META_REPLACE, "Trim section", trimsec);
+            psList *biassecs = psListAlloc(NULL); // Dummy list
+            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.BIASSEC", PS_META_REPLACE, "Bias section", biassecs);
+            psTime *time = psTimeAlloc(PS_TIME_TAI); // Dummy time
+            psMetadataAddPtr(cell->concepts, PS_LIST_TAIL, "CELL.TIME", PS_META_REPLACE, "Time of exposure", time);
+            psMetadataAddS32(cell->concepts, PS_LIST_TAIL, "CELL.TIMESYS", PS_META_REPLACE, "Time system", PS_TIME_TAI);
+        }
+    }
+
+    return true;
+}
+
+// Copy concepts from one FPA to another
+bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
+                       pmFPA *source    // The target FPA
+                      )
+{
+    // Copy FPA concepts
+    target->concepts = pap_psMetadataCopy(target->concepts, source->concepts);
+
+    // Copy chip concepts
+    psArray *targetChips = target->chips; // Chips in target
+    psArray *sourceChips = source->chips; // Chips in source
+    if (targetChips->n != sourceChips->n) {
+        psError(PS_ERR_IO, true, "Number of chips in target (%d) and source (%d) differ --- unable to copy "
+                "concepts.\n", targetChips->n, sourceChips->n);
+        return false;
+    }
+    for (int i = 0; i < targetChips->n; i++) {
+        pmChip *targetChip = targetChips->data[i]; // Target chip of interest
+        pmChip *sourceChip = sourceChips->data[i]; // Source chip of interest
+        if (! targetChip || ! sourceChip) {
+            continue;
+        }
+        targetChip->concepts = pap_psMetadataCopy(targetChip->concepts, sourceChip->concepts);
+
+        // Copy cell concepts
+        psArray *targetCells = targetChip->cells; // Cells in target
+        psArray *sourceCells = sourceChip->cells; // Cells in source
+        if (targetCells->n != sourceCells->n) {
+            psError(PS_ERR_IO, true, "Number of cells in target (%d) and source (%d) differ for chip %d ---"
+                    " unable to copy concepts.\n", targetCells->n, sourceCells->n, i);
+            return false;
+        }
+        for (int j = 0; j < targetCells->n; j++) {
+            pmCell *targetCell = targetCells->data[j]; // Target chip of interest
+            pmCell *sourceCell = sourceCells->data[j]; // Source chip of interest
+            if (! targetCell || ! sourceCell) {
+                continue;
+            }
+            targetCell->concepts = pap_psMetadataCopy(targetCell->concepts, sourceCell->concepts);
+        }
+    }
+
+    return true;
+}
Index: trunk/psModules/src/astrom/pmFPAConceptsGet.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAConceptsGet.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConceptsGet.h	(revision 6872)
@@ -0,0 +1,103 @@
+#ifndef PM_FPA_CONCEPTS_GET_H
+#define PM_FPA_CONCEPTS_GET_H
+
+#include "pslib.h"
+#include "pmAstrometry.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
+                         );
+
+// Set up concepts for blank image
+bool pmFPABlankConcepts(pmFPA *fpa      // The FPA
+                       );
+
+// Copy concepts from one FPA to another
+bool pmFPACopyConcepts(pmFPA *target,   // The target FPA
+                       pmFPA *source    // The target FPA
+                      );
+
+// Retrieve a concept
+psMetadataItem *pmCellGetConcept(pmCell *cell, // The cell
+                                 const char *concept // The concept
+                                );
+
+
+#endif
Index: trunk/psModules/src/astrom/pmFPAConceptsSet.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAConceptsSet.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConceptsSet.c	(revision 6872)
@@ -0,0 +1,904 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmAstrometry.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);
+    }
+
+    return false;
+}
+
+
+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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&mdStatus, dbLookup, "TABLE"); // Name of the table
+        //        const char *colName = psMetadataLookupStr(&mdStatus, dbLookup, "COLUMN"); // Name of the column
+        const char *givenCols = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENDBCOL"); // Name of "where"
+        // columns
+        const char *givenPS = psMetadataLookupStr(&mdStatus, dbLookup, "GIVENPS"); // Values for "where"
+        // columns
+
+        // Now, need to get the "given"s
+        if (strlen(givenCols) || strlen(givenPS)) {
+            psList *cols = psStringSplit(givenCols, ",;"); // List of column names
+            psList *values = psStringSplit(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
+                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");
+    return false;
+}
+
+
+// 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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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 = psMetadataLookupStr(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 = psMetadataLookupStr(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 = psMetadataLookupStr(NULL, cell->camera, "CELL.BIASSEC");
+                psList *checkList = psStringSplit(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 = psMetadataLookupStr(NULL, cell->camera, "CELL.BIASSEC");
+                psList *keywords = psStringSplit(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 = psMetadataLookupStr(NULL, translation, "CELL.XBIN");
+        psString yKeyword = psMetadataLookupStr(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 = psMetadataLookupStr(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 = psMetadataLookupStr(NULL, translation, "CELL.TIME");
+                    psList *dateTimeKeywords = psStringSplit(keywords, " ,;");
+                    psString dateKeyword = psListGet(dateTimeKeywords, PS_LIST_HEAD);
+                    psString timeKeyword = psListGet(dateTimeKeywords, PS_LIST_TAIL);
+
+                    psList *dateTime = psStringSplit(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 = psMetadataLookupStr(&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 = psMetadataLookupStr(&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: trunk/psModules/src/astrom/pmFPAConceptsSet.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAConceptsSet.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConceptsSet.h	(revision 6872)
@@ -0,0 +1,20 @@
+#ifndef PM_FPA_CONCEPTS_SET_H
+#define PM_FPA_CONCEPTS_SET_H
+
+#include "pslib.h"
+#include "pmAstrometry.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: trunk/psModules/src/astrom/pmFPAConstruct.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAConstruct.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConstruct.c	(revision 6872)
@@ -0,0 +1,777 @@
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+#include "pslib.h"
+#include "psMetadataItemParse.h"
+
+#include "pmFPA.h"
+#include "pmConcepts.h"
+#include "pmFPAConstruct.h"
+#include "pmFPAview.h"
+#include "pmFPAUtils.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Read data for a particular cell from the camera format description
+static psMetadata *getCellData(const psMetadata *format, // The camera format description
+                               const char *cellName // The name of the cell
+                              )
+{
+    bool status = true;                 // Result of MD lookup
+    psMetadata *cells = psMetadataLookupMD(&status, format, "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 || !cellData) {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to find specs for cell %s: ignored\n", cellName);
+    }
+
+    return cellData;
+}
+
+// Parse a list of first:second:third pairs in a string
+static bool parseContent(psArray **first, // Array of the first values
+                         psArray **second, // Array of the second values
+                         psArray **third, // Array of the third values
+                         const char *string // The string to parse
+                        )
+{
+    bool allOK = true;                  // Everything was OK?
+    psList *values = psStringSplit(string, " ,;"); // List of the parts
+    *first = psArrayAlloc(values->n);
+    *second = psArrayAlloc(values->n);
+    *third = psArrayAlloc(values->n);
+    int num = 0;
+    psListIterator *valuesIter = psListIteratorAlloc(values, PS_LIST_HEAD, false); // Iterator for values
+    psString value = NULL;               // "first:second:third" string
+    while ((value = psListGetAndIncrement(valuesIter))) {
+        psList *firstSecondThird = psStringSplit(value, ":"); // List containing the first, second, third
+        psArray *fst = psListToArray(firstSecondThird); // An array representation
+        psFree(firstSecondThird);
+        psString firstPart = NULL;      // The first part
+        psString secondPart = NULL;     // The second part
+        psString thirdPart = NULL;      // The third part
+        switch (firstSecondThird->n) {
+        case 1:
+            thirdPart = fst->data[0];
+            break;
+        case 2:
+            secondPart = fst->data[0];
+            thirdPart = fst->data[1];
+            break;
+        case 3:
+            firstPart = fst->data[0];
+            secondPart = fst->data[1];
+            thirdPart = fst->data[2];
+            break;
+        default:
+            psLogMsg(__func__, PS_LOG_WARN, "Badly formated specifier: %s --- ignored.\n", value);
+            allOK = false;
+            psFree(fst);
+            continue;
+        }
+        psArraySet(*first, num, firstPart);
+        psArraySet(*second, num, secondPart);
+        psArraySet(*third, num, thirdPart);
+        num++;
+        psFree(fst);
+    }
+    psFree(valuesIter);
+    psFree(values);
+
+    return allOK;
+}
+
+// Get the name of a PHU chip or cell from the header
+static psString phuNameFromHeader(const char *name, // The name to lookup: "CELL.NAME" or "CHIP.NAME"
+                                  const psMetadata *fileInfo, // FILE within the camera format description
+                                  const psMetadata *header // Primary header
+                                 )
+{
+    bool mdok = true;                   // Result of MD lookup
+    psString keyword = psMetadataLookupStr(&mdok, fileInfo, name);
+    if (!mdok || strlen(keyword) == 0) {
+        return false;
+    }
+    psMetadataItem *resultItem = psMetadataLookup(header, keyword);
+    if (! resultItem) {
+        psError(PS_ERR_IO, false, "Unable to find %s in primary header to identify %s.\n", keyword, name);
+        return NULL;
+    }
+    return psMetadataItemParseString(resultItem);
+}
+
+// Add an HDU to the FPA
+static bool addHDUtoFPA(pmFPA *fpa,     // FPA to which to add
+                        pmHDU *hdu      // HDU to be added
+                       )
+{
+    assert(fpa);
+    assert(hdu);
+
+    if (fpa->hdu) {
+        // Something's already here
+        if (fpa->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since FPA already has one.\n");
+        }
+        return false;
+    }
+    fpa->hdu = psMemIncrRefCounter(hdu);
+    if (hdu->header) {
+        pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, NULL);
+    }
+    pmFPASetFileStatus(fpa, true);
+
+    return true;
+}
+
+// Add an HDU to the chip
+static bool addHDUtoChip(pmChip *chip,  // Chip to which to add
+                         pmHDU *hdu     // HDU to be added
+                        )
+{
+    assert(chip);
+    assert(hdu);
+
+    if (chip->hdu) {
+        // Something's already here
+        if (chip->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since chip already has one.\n");
+        }
+        return false;
+    }
+    chip->hdu = psMemIncrRefCounter(hdu);
+    if (hdu->header) {
+        pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, true, NULL);
+    }
+    pmChipSetFileStatus(chip, true);
+
+    return true;
+}
+
+// Add an HDU to the cell
+static bool addHDUtoCell(pmCell *cell,  // Cell to which to add
+                         pmHDU *hdu     // HDU to be added
+                        )
+{
+    assert(cell);
+    assert(hdu);
+
+    if (cell->hdu) {
+        // Something's already here
+        if (cell->hdu != hdu) {
+            psError(PS_ERR_IO, true, "Unable to add HDU since cell already has one.\n");
+        }
+        return false;
+    }
+    cell->hdu = psMemIncrRefCounter(hdu);
+    if (hdu->header) {
+        pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, true, NULL);
+    }
+    pmCellSetFileStatus(cell, true);
+
+    return true;
+}
+
+
+// Looks up the particular content based on the header
+static const char *getContent(psMetadata *fileInfo, // The FILE from the camera format configuration
+                              psMetadata *contents, // The CONTENTS from the camera format configuration
+                              psMetadata *header // The (primary) header
+                             )
+{
+    bool mdok = true;                   // Status of MD lookup
+    const char *contentHeaders = psMetadataLookupStr(&mdok, fileInfo, "CONTENT"); // Headers for content
+    if (!mdok || !contentHeaders || strlen(contentHeaders) == 0) {
+        psError(PS_ERR_IO, true, "Unable to find CONTENT in FILE within camera format configuration.\n");
+        return NULL;
+    }
+
+    psList *keywords = psStringSplit(contentHeaders, " ,;"); // List of keywords
+    psListIterator *keywordsIter = psListIteratorAlloc(keywords, PS_LIST_HEAD, false); // Iterator
+    psString keyword = NULL;        // Keyword, from iteration
+    psString contentsKey = NULL;    // Key to the CONTENTS menu
+    bool first = true;              // Is it the first value?
+    while ((keyword = psListGetAndIncrement(keywordsIter))) {
+        const char *value = psMetadataLookupStr(&mdok, header, keyword);
+        if (first) {
+            psStringAppend(&contentsKey, "%s", value);
+            first = false;
+        } else {
+            psStringAppend(&contentsKey, "_%s", value);
+        }
+    }
+    psFree(keywordsIter);
+    psFree(keywords);
+    psTrace(__func__, 5, "Looking up %s in the CONTENTS.\n", contentsKey);
+    const char *content = psMetadataLookupStr(&mdok, contents, contentsKey);
+    if (!mdok || !content || strlen(content) == 0) {
+        psError(PS_ERR_IO, true, "Unable to find %s in the CONTENTS.\n", contentsKey);
+        return NULL;
+    }
+    psFree(contentsKey);
+
+    return content;
+}
+
+
+// Given a (string) list of contents "chip:cell:type chip:cell:type", put the HDU in the correct place and
+// plug in the cell configuration information
+static int processContents(pmFPA *fpa,  // The FPA
+                           pmChip *chip, // The chip, or NULL
+                           pmCell *cell, // The cell, or NULL
+                           pmHDU *hdu,  // The HDU to be added
+                           pmFPALevel level, // The level at which to add the HDU
+                           const char *contents, // The contents line, consisting of a list of chip:cell
+                           psMetadata *format // Camera format configuration
+                          )
+{
+    assert(fpa);
+    assert(contents && strlen(contents) > 0);
+    assert(!cell || (cell && chip));    // Need both chip and cell if given a cell
+
+    if (hdu && level == PM_FPA_LEVEL_FPA) {
+        addHDUtoFPA(fpa, hdu);
+    }
+
+    // Parse the list of chip:cell:type
+    psArray *chips = NULL;              // The chips (first bits)
+    psArray *cells = NULL;              // The cells (second bits)
+    psArray *types = NULL;              // The cell types (third bits)
+    int numCells = 0;                   // Number of cells processed
+    parseContent(&chips, &cells, &types, contents);
+    for (int i = 0; i < types->n; i++) {
+        psString chipName = chips->data[i]; // The name of the chip
+        psString cellName = cells->data[i]; // The name of the cell
+        psString cellType = types->data[i]; // The type of the cell
+
+        // Get the chip
+        pmChip *newChip = NULL;         // The chip specified
+        if (chip) {
+            newChip = chip;
+        } else if (chipName) {
+            // Find the chip
+            int chipNum = pmFPAFindChip(fpa, chipName); // The chip we're looking for
+            if (chipNum == -1) {
+                psLogMsg(__func__, PS_LOG_WARN, "Unable to find chip %s in fpa --- ignored.\n", chipName);
+                continue;
+            }
+            newChip = fpa->chips->data[chipNum];
+        }
+
+        if (!newChip) {
+            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine chip for entry %d of content (follows) --- "
+                     "ignored.\n\t%s", i, contents);
+            continue;
+        }
+
+        // Get the cell
+        pmCell *newCell = NULL;         // The cell specified
+        if (cell) {
+            newCell = cell;
+        } else if (cellName) {
+            // Find the cell
+            int cellNum = pmChipFindCell(newChip, cellName); // The cell we're looking for
+            if (cellNum == -1) {
+                psLogMsg(__func__, PS_LOG_WARN, "Unable to find cell %s in chip %s --- ignored.\n",
+                         cellName, chipName);
+                continue;
+            }
+            newCell = newChip->cells->data[cellNum];
+        }
+        if (!newCell) {
+            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine cell for entry %d of content (follows) --- "
+                     "ignored.\n\t%s", i, contents);
+            continue;
+        }
+
+        // Get the type
+        if (!cellType) {
+            psLogMsg(__func__, PS_LOG_WARN, "Unable to determine cell type for entry %d of content (follows) "
+                     "--- ignored.\n\t%s", i, contents);
+            continue;
+        }
+        psMetadata *cellData = getCellData(format, cellType); // Data for this cell
+
+        // Put in the HDU
+        if (hdu && level == PM_FPA_LEVEL_CHIP) {
+            addHDUtoChip(newChip, hdu);
+        }
+        if (hdu && level == PM_FPA_LEVEL_CELL) {
+            addHDUtoCell(newCell, hdu);
+        }
+
+        // Put in the cell data
+        if (newCell->config) {
+            psLogMsg(__func__, PS_LOG_WARN, "Overwriting cell data in chip %s, cell %s\n", chipName,
+                     cellName);
+            psFree(newCell->config); // Make way!
+        }
+        newCell->config = psMemIncrRefCounter(cellData);
+        pmConceptsReadCell(newCell, PM_CONCEPT_SOURCE_CAMERA | PM_CONCEPT_SOURCE_DEFAULTS, false, NULL);
+        numCells++;
+    }
+    psFree(chips);
+    psFree(cells);
+    psFree(types);
+
+    return numCells;
+}
+
+static pmFPALevel hduLevel(psMetadata *format // The camera format configuration
+                          )
+{
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *file = psMetadataLookupMD(&mdok, format, "FILE"); // File information
+    if (!mdok || !file) {
+        psError(PS_ERR_IO, true, "Unable to find FILE information in camera format configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+    const char *extType = psMetadataLookupStr(&mdok, file, "EXTENSIONS");
+    if (!mdok || !extType || strlen(extType) == 0) {
+        psError(PS_ERR_IO, true, "Unable to find EXTENSIONS in the FILE information in the camera format"
+                " configuration.\n");
+        return PM_FPA_LEVEL_NONE;
+    }
+
+    // Where do we stick in the HDUs?
+    pmFPALevel level = PM_FPA_LEVEL_NONE; // Level for HDU insertion
+    if (strcasecmp(extType, "CHIP") == 0) {
+        level = PM_FPA_LEVEL_CHIP;
+    } else if (strcasecmp(extType, "CELL") == 0) {
+        level = PM_FPA_LEVEL_CELL;
+    } else if (strcasecmp(extType, "NONE") != 0) {
+        psError(PS_ERR_IO, true, "EXTENSIONS is not CHIP or CELL or NONE.\n");
+    }
+
+    return level;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Construct an FPA instance on the basis of a camera configuration
+pmFPA *pmFPAConstruct(const psMetadata *camera // The camera configuration
+                     )
+{
+    pmFPA *fpa = pmFPAAlloc(camera);    // The FPA to fill out
+
+    bool mdok = true;                   // Status from MD lookups
+    psMetadata *components = psMetadataLookupMD(&mdok, camera, "FPA"); // FPA components
+    psMetadataIterator *componentsIter = psMetadataIteratorAlloc(components, PS_LIST_HEAD, NULL);
+    psMetadataItem *componentsItem = NULL; // Item from components
+    while ((componentsItem = psMetadataGetAndIncrement(componentsIter))) {
+        const char *chipName = componentsItem->name; // Name of the chip
+        if (componentsItem->type != PS_DATA_STRING) {
+            psLogMsg(__func__, PS_LOG_WARN, "Element %s in FPA within the camera configuration is not of "
+                     "type STR (type=%x) --- ignored.\n", chipName, componentsItem->type);
+            continue;
+        }
+        pmChip *chip = pmChipAlloc(fpa, chipName); // The chip
+        psList *cellNames = psStringSplit(componentsItem->data.V, " ,;"); // List of cell names
+        psListIterator *cellNamesIter = psListIteratorAlloc(cellNames, PS_LIST_HEAD, false); // Iterator
+        psString cellName = NULL;       // Name of cell
+        while ((cellName = psListGetAndIncrement(cellNamesIter))) {
+            pmCell *cell = pmCellAlloc(chip, cellName); // New cell
+            psFree(cell);               // Drop reference
+        }
+        psFree(chip);                   // Drop reference
+        psFree(cellNamesIter);
+        psFree(cellNames);
+    }
+    psFree(componentsIter);
+
+    return fpa;
+}
+
+bool pmFPAAddSourceFromView(pmFPA *fpa,   // The FPA
+                            const pmFPAview *phuView, // The view, corresponding to the PHU
+                            psMetadata *format // Format of file
+                           )
+{
+    assert(fpa);
+    assert(phuView);
+    assert(format);
+
+    // Where does the PHU go?
+    bool mdok = true;                   // Status of MD lookup
+    psMetadata *fileInfo = psMetadataLookupMD(&mdok, format, "FILE"); // File information from the format
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE information in the camera format configuration.\n");
+        return false;
+    }
+    const char *phuType = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // What is the PHU?
+    if (!mdok || strlen(phuType) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find PHU in the FILE information of the camera format.\n");
+        return false;
+    }
+
+    // Generate the PHU
+    pmHDU *phdu = pmHDUAlloc("PHU");    // The primary header data unit
+    phdu->format = psMemIncrRefCounter(format);
+
+    // Put in the PHU
+    pmChip *chip = NULL;                // The chip that corresponds to the PHU
+    pmCell *cell = NULL;                // The cell that corresponds to the PHU
+    if (strcasecmp(phuType, "FPA") == 0) {
+        addHDUtoFPA(fpa, phdu);
+        psFree(phdu);
+    } else {
+        psArray *chips = fpa->chips;    // Array of chips
+        if (phuView->chip < 0 || phuView->chip > chips->n) {
+            psError(PS_ERR_IO, true, "PHU specified by the camera format requires specification of a "
+                    "particular chip, which cannot be determined from the view (%d).\n", phuView->chip);
+            psFree(phdu);
+            return false;
+        }
+        chip = chips->data[phuView->chip];
+        if (strcasecmp(phuType, "CHIP") == 0) {
+            addHDUtoChip(chip, phdu);
+            psFree(phdu);
+        } else if (strcasecmp(phuType, "CELL") == 0) {
+            psArray *cells = chip->cells; // Array of cells
+            if (phuView->cell < 0 || phuView->cell > cells->n) {
+                psError(PS_ERR_IO, true, "PHU specified by the camera format requires specification of a "
+                        "particular cell, which cannot be determined from the view (%d).\n", phuView->cell);
+                psFree(phdu);
+                return false;
+            }
+            addHDUtoCell(cell, phdu);
+            psFree(phdu);
+        } else {
+            psError(PS_ERR_IO, true, "PHU in the camera configuration format is not FPA, CHIP or CELL.\n");
+            psFree(phdu);
+            return false;
+        }
+    }
+
+    // Put in the extensions
+    pmFPALevel level = hduLevel(format);// The level for the HDUs to go
+    if (level == PM_FPA_LEVEL_NONE) {
+        // No extensions --- we're done
+        return true;
+    }
+    psMetadata *contents = psMetadataLookupMD(&mdok, format, "CONTENTS"); // The contents of the FITS file
+    if (!mdok || !contents) {
+        psError(PS_ERR_IO, false, "Unable to find CONTENTS in the camera format configuration.\n");
+        return false;
+    }
+
+    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL); // Iterator
+    psMetadataItem *contentsItem = NULL;// Item from the contents iteration
+    while ((contentsItem = psMetadataGetAndIncrement(contentsIter))) {
+        if (contentsItem->type != PS_DATA_STRING) {
+            psLogMsg(__func__, PS_LOG_WARN, "Content item %s is not of type STR --- ignored.\n",
+                     contentsItem->name);
+            continue;
+        }
+        pmHDU *hdu = pmHDUAlloc(contentsItem->name); // HDU to add
+        hdu->format = psMemIncrRefCounter(format);
+        const char *content = contentsItem->data.V; // The content data
+        processContents(fpa, chip, cell, hdu, level, content, format);
+        psFree(hdu);
+    }
+    psFree(contentsIter);
+
+    return true;
+}
+
+
+
+// Add an input file to the FPA
+pmFPAview *pmFPAAddSourceFromHeader(pmFPA *fpa, // The FPA
+                                    psMetadata *phu, // Primary header of file
+                                    psMetadata *format // Format of file
+                                   )
+{
+    assert(fpa);
+    assert(phu);
+    assert(format);
+
+    bool mdok = true;                   // Status from metadata lookups
+    psMetadata *fileInfo = psMetadataLookupMD(&mdok, format, "FILE"); // The file information
+    if (!mdok || !fileInfo) {
+        psError(PS_ERR_IO, false, "Unable to find FILE in the camera format configuration.\n");
+        return NULL;
+    }
+
+    // Check the name of the FPA
+    psString newFPAname = phuNameFromHeader("FPA.NAME", fileInfo, phu); // New name for the FPA
+    const char *currentFPAname = psMetadataLookupStr(&mdok, fpa->concepts, "FPA.NAME"); // Current name
+    if (mdok && currentFPAname && strlen(currentFPAname) > 0 && strcmp(currentFPAname, newFPAname) != 0) {
+        psLogMsg(__func__, PS_LOG_WARN, "FPA.NAME for new source (%s) doesn't match FPA.NAME for current "
+                 "fpa (%s).\n", newFPAname, currentFPAname);
+    }
+    psMetadataAddStr(fpa->concepts, PS_LIST_HEAD, "FPA.NAME", PS_META_REPLACE, "Name of FPA", newFPAname);
+    psFree(newFPAname);                 // Drop reference
+
+    // Where does the PHU go?
+    const char *phuType = psMetadataLookupStr(&mdok, fileInfo, "PHU"); // What is the PHU?
+    if (!mdok || strlen(phuType) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find PHU in the format specification.\n");
+        return NULL;
+    }
+    pmHDU *phdu = pmHDUAlloc("PHU");    // The primary header data unit
+    phdu->header = psMemIncrRefCounter(phu);
+    phdu->format = psMemIncrRefCounter(format);
+    // Generate the view
+    pmFPAview *view = pmFPAviewAlloc(0); // The FPA view corresponding to the PHU, to be returned
+    view->chip = -1;
+    view->cell = -1;
+    view->readout = -1;
+
+    // And what are the individual extensions?
+    const char *extType = psMetadataLookupStr(&mdok, fileInfo, "EXTENSIONS"); // What's in the extns?
+    if (!mdok || strlen(extType) == 0) {
+        psError(PS_ERR_IO, false, "Unable to find EXTENSIONS in the format specification.\n");
+        psFree(view);
+        return NULL;
+    }
+
+
+    // This is a special case: PHU=FPA and EXTENSIONS=NONE.  The only reason to do this is in the case of
+    // single CCD imagers, where the entire FPA is in the PHU image.  In this case, the CONTENTS is of type
+    // STR (rather than METADATA), and has the usual chip:cell.
+    if (strcasecmp(phuType, "FPA") == 0 && strcasecmp(extType, "NONE") == 0) {
+        const char *contents = psMetadataLookupStr(&mdok, format, "CONTENTS"); // The contents of the file
+        if (!mdok || !contents || strlen(contents) == 0) {
+            psError(PS_ERR_IO, true, "Unable to find CONTENTS in the camera format configuration.\n");
+            psFree(view);
+            return NULL;
+        }
+
+        processContents(fpa, NULL, NULL, phdu, PM_FPA_LEVEL_FPA, contents, format);
+        psFree(phdu);
+        return view;
+    }
+
+    // In all other cases, the CONTENTS is of type METADATA, and is either a menu if EXTENSIONS=NONE, or
+    // a list of extensions otherwise.
+    psMetadata *contents = psMetadataLookupMD(&mdok, format, "CONTENTS"); // The contents of the FITS file
+    if (!mdok || !contents) {
+        psError(PS_ERR_IO, false, "Unable to find CONTENTS in the camera format configuration.\n");
+        psFree(view);
+        return NULL;
+    }
+
+    // No extensions --- only have the PHU.  In this case, the CONTENTS is a menu, with CONTENT indicating
+    // header keywords that provides a key to the CONTENTS menu.
+    if (strcasecmp(extType, "NONE") == 0) {
+        // We have already dealt with the case PHU=FPA, in a special case, above.
+        const char *content = getContent(fileInfo, contents, phu); // The content: string of chip:cell pairs
+
+        // Need to look up what chip we have.
+        psString chipName = phuNameFromHeader("CHIP.NAME", fileInfo, phu);
+        psTrace(__func__, 5, "This is chip %s\n", chipName);
+        int chipNum = pmFPAFindChip(fpa, chipName); // Chip number
+        if (chipNum == -1) {
+            psError(PS_ERR_IO, true, "Unable to find chip %s in FPA.\n", chipName);
+            psFree(view);
+            return NULL;
+        }
+        pmChip *chip = fpa->chips->data[chipNum]; // Chip of interest
+        view->chip = chipNum;
+
+        pmCell *cell = NULL;            // Cell of interest
+
+        pmFPALevel level = PM_FPA_LEVEL_NONE; // Level for HDU to be added
+        if (strcasecmp(phuType, "CHIP") == 0) {
+            level = PM_FPA_LEVEL_CHIP;
+        } else if (strcasecmp(phuType, "CELL") == 0) {
+            level = PM_FPA_LEVEL_CELL;
+            // Need to look up what cell we have.
+            psString cellName = phuNameFromHeader("CELL.NAME", fileInfo, phu);
+            int cellNum = pmChipFindCell(chip, cellName); // Cell number
+            if (cellNum == -1) {
+                psError(PS_ERR_IO, true, "Unable to find cell %s in chip %s.\n", cellName, chipName);
+                psFree(view);
+                return NULL;
+            }
+            cell = chip->cells->data[cellNum];
+            view->cell = cellNum;
+            psFree(cellName);
+        } else {
+            psError(PS_ERR_IO, true, "PHU is not FPA, CHIP or CELL.\n");
+            psFree(view);
+            return NULL;
+        }
+        psFree(chipName);
+
+        processContents(fpa, chip, cell, phdu, level, content, format);
+        psFree(phdu);
+        return view;
+    }
+
+
+    // From here on, we have extensions that we iterate through.  The CONTENTS is a list of extensions.
+    pmChip *chip = NULL;                // The chip of interest
+    pmCell *cell = NULL;                // The cell of interest
+
+    // First, put in the PHU
+    if (strcasecmp(phuType, "FPA") == 0) {
+        addHDUtoFPA(fpa, phdu);
+    } else {
+        // Get the chip
+        psString chipName = phuNameFromHeader("CHIP.NAME", fileInfo, phu); // Name of the chip
+        int chipNum = pmFPAFindChip(fpa, chipName); // Chip number
+        if (chipNum == -1) {
+            psError(PS_ERR_IO, true, "Unable to find chip %s in FPA.\n", chipName);
+            psFree(view);
+            return NULL;
+        }
+        chip = fpa->chips->data[chipNum]; // The specified chip
+        view->chip = chipNum;
+
+        if (strcasecmp(phuType, "CHIP") == 0) {
+            addHDUtoChip(chip, phdu);
+        } else if (strcasecmp(phuType, "CELL") == 0) {
+            psString cellName = phuNameFromHeader("CELL.NAME", fileInfo, phu); // Name of the cell
+            int cellNum = pmChipFindCell(chip, cellName); // Cell number
+            if (cellNum == -1) {
+                psError(PS_ERR_IO, true, "Unable to find cell %s in chip %s.\n", cellName, chipName);
+                psFree(view);
+                return NULL;
+            }
+            cell = chip->cells->data[cellNum]; // The specified cell
+            view->cell = cellNum;
+            psFree(cellName);
+
+            addHDUtoCell(cell, phdu);
+        } else {
+            psError(PS_ERR_IO, true, "The format of the PHU (%s) is not FPA, CHIP or CELL.\n", phuType);
+            psFree(view);
+            return NULL;
+        }
+        psFree(chipName);
+    }
+    psFree(phdu);
+
+    pmFPALevel level = hduLevel(format);// The level at which to plug in HDU
+
+    // Now go through the contents
+    psMetadataIterator *contentsIter = psMetadataIteratorAlloc(contents, PS_LIST_HEAD, NULL);
+    psMetadataItem *contentsItem = NULL; // Item from contents
+    while ((contentsItem = psMetadataGetAndIncrement(contentsIter))) {
+        const char *extName = contentsItem->name;
+        if (contentsItem->type != PS_DATA_STRING) {
+            psLogMsg(__func__, PS_LOG_WARN, "CONTENTS item %s is not of type STR --- ignored.\n", extName);
+            continue;
+        }
+
+        pmHDU *hdu = pmHDUAlloc(extName); // The extension
+        hdu->format = psMemIncrRefCounter(format);
+
+        processContents(fpa, chip, cell, hdu, level, contentsItem->data.V, format);
+        psFree(hdu);
+    }
+    psFree(contentsIter);
+
+    pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_DEFAULTS, NULL);
+
+    return view;
+}
+
+
+// Print out the focal plane structure
+void pmFPAPrint(pmFPA *fpa,             // FPA to print
+                bool header,            // Print headers?
+                bool concepts           // Print concepts?
+               )
+{
+    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 read in for extension %s\n", fpa->hdu->extname);
+        }
+        if (header) {
+            if (fpa->hdu->header) {
+                psTrace(__func__, 2, "---> Header:\n");
+                psMetadataPrint(fpa->hdu->header, 8);
+            } else {
+                psTrace(__func__, 2, "---> NO HEADER read in for extension %s\n", fpa->hdu->extname);
+            }
+        }
+    }
+    if (concepts) {
+        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 (header) {
+                if (chip->hdu->header) {
+                    psTrace(__func__, 4, "---> Header:\n");
+                    psMetadataPrint(chip->hdu->header, 8);
+                } else {
+                    psTrace(__func__, 4, "---> NO HEADER read in for extension %s\n", chip->hdu->extname);
+                }
+            }
+            if (! chip->hdu->images) {
+                psTrace(__func__, 4, "---> NO PIXELS read in for extension %s\n", chip->hdu->extname);
+            }
+        }
+        if (concepts) {
+            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 (header) {
+                    if (cell->hdu->header) {
+                        psTrace(__func__, 6, "---> Header:\n");
+                        psMetadataPrint(cell->hdu->header, 8);
+                    } else {
+                        psTrace(__func__, 6, "---> NO HEADER read in for extension %s\n", cell->hdu->extname);
+                    }
+                }
+                if (! cell->hdu->images) {
+                    psTrace(__func__, 6, "---> NO PIXELS read in for extension %s\n", cell->hdu->extname);
+                }
+            }
+            if (concepts) {
+                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
+                psList *bias = readout->bias; // The list of bias images
+                if (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);
+                }
+                if (bias) {
+                    psListIterator *biasIter = psListIteratorAlloc(bias, PS_LIST_HEAD, false); // Iterator
+                    psImage *biasImage = NULL; // Bias image from iteration
+                    while ((biasImage = psListGetAndIncrement(biasIter))) {
+                        psTrace(__func__, 8, "Bias:  [%d:%d,%d:%d] (%dx%d)\n", biasImage->col0,
+                                biasImage->col0 + biasImage->numCols, biasImage->row0,
+                                biasImage->row0 + biasImage->numRows, biasImage->numCols, biasImage->numRows);
+                    }
+                    psFree(biasIter);
+                }
+            } // Iterating over cell
+        } // Iterating over chip
+    } // Iterating over FPA
+
+}
Index: trunk/psModules/src/astrom/pmFPAConstruct.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAConstruct.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAConstruct.h	(revision 6872)
@@ -0,0 +1,28 @@
+#ifndef PM_FPA_CONSTRUCT_H
+#define PM_FPA_CONSTRUCT_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+
+// Construct an FPA instance on the basis of a camera configuration
+pmFPA *pmFPAConstruct(const psMetadata *camera // The camera configuration
+                     );
+
+bool pmFPAAddSourceFromView(pmFPA *fpa,   // The FPA
+                            const pmFPAview *phuView, // The view, corresponding to the PHU
+                            psMetadata *format // Format of file
+                           );
+
+pmFPAview *pmFPAAddSourceFromHeader(pmFPA *fpa, // The FPA
+                                    psMetadata *phu, // Primary header of file
+                                    psMetadata *format // Format of file
+                                   );
+
+// Print out the FPA
+void pmFPAPrint(pmFPA *fpa,             // FPA to print
+                bool header,            // Print headers?
+                bool concepts           // Print concepts?
+               );
+
+#endif
Index: trunk/psModules/src/astrom/pmFPACopy.c
===================================================================
--- trunk/psModules/src/astrom/pmFPACopy.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPACopy.c	(revision 6872)
@@ -0,0 +1,692 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+#include "psImageFlip.h"
+#include "psRegionIsBad.h"
+
+#include "pmFPA.h"
+#include "pmFPAUtils.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPACopy.h"
+
+#define MAX(x,y) ((x) > (y) ? (x) : (y))
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Add cells in a chip to a list
+static bool addCellsFromChip(psList *list, // List of cells
+                             const pmChip *chip // The chip from which to add cells
+                            )
+{
+    assert(list);
+    assert(chip);
+
+    psArray *cells = chip->cells;       // Array of cells
+    bool result = true;                 // Result of adding cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // A cell
+        result |= psListAdd(list, PS_LIST_TAIL, cell);
+    }
+
+    return result;
+}
+
+// Add cells in an FPA to a list
+static bool addCellsFromFPA(psList *list, // List of cells
+                            const pmFPA *fpa // The FPA from which to add cells
+                           )
+{
+    assert(list);
+    assert(fpa);
+
+    psArray *chips = fpa->chips;        // Array of chips
+    bool result = true;                 // Result of adding cells
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // A chip
+        result |= addCellsFromChip(list, chip);
+    }
+
+    return result;
+}
+
+// Get a list of cells that share the HDU for the target cell
+static bool cellList(psList *targets,   // The list of target cells
+                     psList *sources,   // The list of source cells
+                     pmCell *targetCell, // The target cell
+                     pmCell *sourceCell // The source cell
+                    )
+{
+    assert(targetCell);
+    assert(sourceCell);
+
+    if (targetCell->hdu) {
+        if (targets) {
+            psListAdd(targets, PS_LIST_TAIL, targetCell);
+        }
+        if (sources) {
+            psListAdd(sources, PS_LIST_TAIL, sourceCell);
+        }
+    } else {
+        pmChip *targetChip = targetCell->parent; // The target parent chip
+        pmChip *sourceChip = sourceCell->parent; // The source parent chip
+        if (targetChip->hdu) {
+            if (targets) {
+                addCellsFromChip(targets, targetChip);
+            }
+            if (sources) {
+                addCellsFromChip(sources, sourceChip);
+            }
+        } else {
+            pmFPA *targetFPA = targetChip->parent; // The target parent FPA
+            pmFPA *sourceFPA = sourceChip->parent; // The source parent FPA
+            if (targetFPA->hdu) {
+                if (targets) {
+                    addCellsFromFPA(targets, targetFPA);
+                }
+                if (sources) {
+                    addCellsFromFPA(sources, sourceFPA);
+                }
+            } else {
+                psError(PS_ERR_IO, true, "Unable to find HDU for cell to generate list!\n");
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+// Get the maximum extent of the HDU from the trimsec and biassecs
+static bool sizeHDU(int *xSize, int *ySize, // Size of HDU
+                    psList *cells       // List of cells
+                   )
+{
+    psListIterator *cellsIter = psListIteratorAlloc(cells, PS_LIST_HEAD, false); // Iterator for cells
+    pmCell *cell = NULL;                // The cell from iteration
+    bool mdok = true;                   // Status of MD lookup
+    *xSize = 0;
+    *ySize = 0;
+    while ((cell = psListGetAndIncrement(cellsIter))) {
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.TRIMSEC"); // Trim section
+        if (mdok && trimsec && !psRegionIsBad(*trimsec)) {
+            *xSize = MAX(trimsec->x1, *xSize);
+            *ySize = MAX(trimsec->y1, *ySize);
+        } else {
+            psFree(cellsIter);
+            return false;
+        }
+        psList *biassecs = psMetadataLookupPtr(&mdok, cell->concepts, "CELL.BIASSEC"); // Bias sections
+        if (mdok && biassecs) {
+            psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+            psRegion *biassec = NULL;   // The bias section
+            while ((biassec = psListGetAndIncrement(biassecsIter))) {
+                if (!psRegionIsBad(*trimsec)) {
+                    *xSize = MAX(biassec->x1, *xSize);
+                    *ySize = MAX(biassec->y1, *ySize);
+                } else {
+                    psFree(biassecsIter);
+                    psFree(cellsIter);
+                    return false;
+                }
+            }
+            psFree(biassecsIter);
+        }
+    }
+    psFree(cellsIter);
+
+    return (*xSize != 0 && *ySize != 0);
+}
+
+
+static psRegion sectionForImage(int *position, // Position on the output image, updated
+                                const psImage *image, // Image containing the sizes and offsets
+                                int readdir // Read direction, 1=rows, 2=cols
+                               )
+{
+    psRegion region;
+    switch (readdir) {
+    case 1:                           // Read direction is rows
+        region = psRegionSet(*position, *position + image->numCols, image->row0,
+                             image->row0 + image->numRows);
+        *position += image->numCols;
+        break;
+    case 2:                           // Read direction is columns
+        region = psRegionSet(image->col0, image->col0 + image->numCols, *position,
+                             *position + image->numRows);
+        *position += image->numRows;
+        break;
+    default:
+        psAbort(__func__, "Shouldn't ever get here!\n");
+    }
+
+    return region;
+}
+
+static bool doBiasSections(int *position, // Position on the output image, updated
+                           pmCell *target, // Target cell
+                           const pmCell *source // Source cell
+                          )
+{
+    psMetadataItem *biassecItem = psMetadataLookup(target->concepts, "CELL.BIASSEC"); // Bias sections
+    if (!biassecItem) {
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.BIASSEC has not been initialised in target cell --- "
+                 "ignored.\n");
+        return false;
+    }
+    psFree(biassecItem->data.V);        // Blow away the old list
+    psList *biassecs = psListAlloc(NULL);
+    biassecItem->data.V = biassecs;
+
+    bool mdok = true;                   // Status of MD lookup
+    int readdir = psMetadataLookupS32(&mdok, source->concepts, "CELL.READDIR"); // Read direction
+    if (!mdok || (readdir != 1 && readdir != 2)) {
+        // Probably unnecessary, but just in case....
+        psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR is not set in source cell --- ignored.\n");
+        return false;
+    }
+
+    pmReadout *readout = source->readouts->data[0]; // The first source readout, as representative
+    psList *biases = readout->bias; // The bias images from the source readout
+
+    psListIterator *biasIter = psListIteratorAlloc(biases, PS_LIST_HEAD, true); // Iterator for biases
+    psImage *bias = NULL;       // Bias image from iteration
+    while ((bias = psListGetAndIncrement(biasIter))) {
+        // Construct a region
+        psRegion *biassec = psAlloc(sizeof(psRegion)); // The new region; need a psMemBlock
+        *biassec = sectionForImage(position, bias, readdir);
+        psListAdd(biassecs, PS_LIST_TAIL, biassec);
+        psFree(biassec);        // Drop reference
+    }
+    psFree(biasIter);
+
+    return true;
+}
+
+// Generate CELL.TRIMSEC and CELL.BIASSEC for the target cells
+static bool generateTrimBias(psList *targets, // List of target cells
+                             psList *sources // List of source cells
+                            )
+{
+    pmCell *target = NULL, *source = NULL; // Cells from iteration
+    int numCells = targets->n;          // Number of cells
+    int cellNum = 0;                    // The cell number
+    int position = 0;                   // Position on the image
+    bool mdok = true;                   // Status of MD lookup
+
+    // First run through to do the LHS biases
+    psListIterator *targetsIter = psListIteratorAlloc(targets, PS_LIST_HEAD, false); // Iterator for targets
+    psListIterator *sourcesIter = psListIteratorAlloc(sources, PS_LIST_HEAD, false); // Iterator for sources
+    bool done = false;                   // Done with iteration?
+    while ((target = psListGetAndIncrement(targetsIter)) && (source = psListGetAndIncrement(sourcesIter)) &&
+            !done) {
+        if (cellNum <= numCells/2 - 1) {
+            doBiasSections(&position, target, source);
+            cellNum++;
+        } else {
+            done = true;
+        }
+    }
+
+    // Second run through to do the trim sections
+    psListIteratorSet(targetsIter, PS_LIST_HEAD);
+    psListIteratorSet(sourcesIter, PS_LIST_HEAD);
+    while ((target = psListGetAndIncrement(targetsIter)) && (source = psListGetAndIncrement(sourcesIter))) {
+        psRegion *trimsec = psMetadataLookupPtr(&mdok, target->concepts, "CELL.TRIMSEC"); // Trim section
+        if (!mdok || !trimsec) {
+            psLogMsg(__func__, PS_LOG_WARN, "CELL.TRIMSEC has not been initialised in target cell --- "
+                     "ignored.\n");
+            continue;
+        }
+
+        int readdir = psMetadataLookupS32(&mdok, source->concepts, "CELL.READDIR"); // Read direction
+        if (!mdok || (readdir != 1 && readdir != 2)) {
+            // Probably unnecessary, but just in case....
+            psLogMsg(__func__, PS_LOG_WARN, "CELL.READDIR is not set in source cell --- ignored.\n");
+            continue;
+        }
+
+        pmReadout *readout = source->readouts->data[0]; // The first source readout, as representative
+        psImage *image = readout->image;// The proper image
+        *trimsec = sectionForImage(&position, image, readdir);
+    }
+
+    // A final run through to do the RHS biases
+    psListIteratorSet(targetsIter, cellNum);
+    psListIteratorSet(sourcesIter, cellNum);
+    while ((target = psListGetAndIncrement(targetsIter)) && (source = psListGetAndIncrement(sourcesIter))) {
+        doBiasSections(&position, target, source);
+    }
+
+    // Clean up
+    psFree(targetsIter);
+    psFree(sourcesIter);
+
+    return (position > 0);
+}
+
+
+// Generate an HDU with the pixels
+static bool generateHDU(pmCell *target,  // The target cell
+                        pmCell *source, // The source cell
+                        int xBin, int yBin // Binning in x and y
+                       )
+{
+    // Get the HDU and a list of cells below it
+    pmHDU *hdu = pmHDUFromCell(target); // The HDU in the target cell
+    psList *targetCells = psListAlloc(NULL); // List of target cells below the target HDU
+    psList *sourceCells = psListAlloc(NULL); // List of source cells below the target HDU
+    if (! cellList(targetCells, sourceCells, target, source)) {
+        psError(PS_ERR_IO, true, "Unable to find cells to generate HDU!\n");
+        return false;
+    }
+
+    // Check the number of readouts
+    int numReadouts = -1;               // Number of readouts
+    {
+        psListIterator *iter = psListIteratorAlloc(sourceCells, PS_LIST_HEAD, false); // Iterator for cells
+        pmCell *cell = NULL;                // The cell from iteration
+        while ((cell = psListGetAndIncrement(iter)))
+        {
+            psArray *readouts = cell->readouts;
+            if (numReadouts == -1) {
+                numReadouts = readouts->n;
+            } else if (readouts->n != numReadouts) {
+                psError(PS_ERR_IO, true, "Number of readouts doesn't match: %d vs %d\n", readouts->n,
+                        numReadouts);
+                return false;
+            }
+
+        }
+        psFree(iter);
+    }
+
+    // Get the size of the HDU, either from existing trimsec and biassec, or generate these and try again
+    int xSize = 0, ySize = 0;           // Size of HDU
+    if (!sizeHDU(&xSize, &ySize, targetCells) && !(generateTrimBias(targetCells, sourceCells) &&
+            sizeHDU(&xSize, &ySize, targetCells))) {
+        psError(PS_ERR_IO, true, "Unable to determine size of HDU!\n");
+        return false;
+    }
+    psFree(targetCells);
+    psFree(sourceCells);
+
+    xSize = (int)ceilf((float)xSize/(float)xBin);
+    ySize = (int)ceilf((float)ySize/(float)yBin);
+
+    hdu->images = psArrayAlloc(numReadouts);
+    for (int i = 0; i < numReadouts; i++) {
+        psImage *image = psImageAlloc(xSize, ySize, PS_TYPE_F32);
+        psImageInit(image, 0.0);
+        hdu->images->data[i] = image;
+    }
+
+    return true;
+}
+
+// Copy pixels from a target image to a source image, with flips
+static bool copyPixels(psImage *target, // Target image (HDU pixels)
+                       psImage *source, // Source image (from source cell)
+                       psRegion region, // Region for pasting
+                       bool xFlip,      // Flip in x?
+                       bool yFlip       // Flip in y?
+                      )
+{
+    psImage *overlay = psMemIncrRefCounter(source);
+    if (xFlip) {
+        psImage *temp = psImageFlipX(overlay);
+        psFree(overlay);
+        overlay = temp;
+    }
+    if (yFlip) {
+        psImage *temp = psImageFlipY(overlay);
+        psFree(overlay);
+        overlay = temp;
+    }
+    int numPix = psImageOverlaySection(target, overlay, region.x0, region.y0, "=");
+    psFree(overlay);
+    return (numPix > 0);
+}
+
+// Bin a region down by specified factors in x and y
+static void binRegion(psRegion *region, // Region to be binned
+                      int xBin, int yBin// Binning in x and y
+                     )
+{
+    // Want to include the lower bound: 1 binned by 4 --> 0; 3 binned by 4 --> 0; 4 binned by 4 --> 1
+    region->x0 = (int)(region->x0 / xBin);
+    region->y0 = (int)(region->y0 / yBin);
+    // Want to exclude the upper bound: 4 binned by 4 --> 1; 5 binned by 4 --> 2; 7 binned by 4 --> 2
+    region->x1 = (int)((region->x1 + xBin - 1) / xBin);
+    region->y1 = (int)((region->y1 + yBin - 1) / yBin);
+}
+
+
+static pmHDU *findPHU(const pmCell *cell// The cell for which to find the PHU
+                     )
+{
+    if (cell->hdu && cell->hdu->phu) {
+        return cell->hdu;
+    }
+    pmChip *chip = cell->parent;        // The parent chip
+    if (chip->hdu && chip->hdu->phu) {
+        return chip->hdu;
+    }
+    pmFPA *fpa = chip->parent;  // The parent FPA
+    if (fpa->hdu && fpa->hdu->phu) {
+        return fpa->hdu;
+    }
+
+    psError(PS_ERR_IO, true, "Unable to find PHU for target cell.\n");
+    return NULL;
+}
+
+
+static bool copyPHU(pmCell *targetCell, // The target cell
+                    pmCell *sourceCell  // The source cell
+                   )
+{
+    // Find the respective PHUs
+    pmHDU *targetPHU = findPHU(targetCell); // The target PHU
+    if (targetPHU->header) {
+        return false;                   // No work required
+    }
+    // Copy the header over
+    pmHDU *sourcePHU = findPHU(sourceCell); // The source PHU
+    targetPHU->header = psMetadataCopy(NULL, sourcePHU->header);
+    return true;
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static engine functions --- these do all the work.  Actually, cellCopy does all the work; the others
+// merely iterate on the higher-level components.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static int cellCopy(pmCell *target,     // The target cell
+                    pmCell *source,     // The source cell, to be copied
+                    bool pixels,        // Copy the pixels?
+                    int xBin, int yBin  // (Relative) binning factors in x and y
+                   )
+{
+    assert(target);
+    assert(source);
+
+    psArray *sourceReadouts = source->readouts; // The source readouts
+    int numReadouts = sourceReadouts->n; // Number of readouts copied
+
+    // Copy any headers
+    if (target->hdu && !target->hdu->phu) {
+        pmHDU *sourceHDU = pmHDUFromCell(source);
+        target->hdu->header = psMetadataCopy(target->hdu->header, sourceHDU->header);
+    }
+
+    pmHDU *hdu = pmHDUFromCell(target); // The target HDU; we need to fix this up
+    if (!hdu->images) {
+        generateHDU(target, source, xBin, yBin);
+    }
+    if (!hdu->header) {
+        hdu->header = psMetadataAlloc();
+    }
+    // Copy the PHU over as well, if required
+    copyPHU(target, source);
+
+    // Need to check/change CELL.XPARITY and CELL.YPARITY
+    bool xFlip = (psMetadataLookupS32(NULL, target->concepts, "CELL.XPARITY") !=
+                  psMetadataLookupS32(NULL, source->concepts, "CELL.XPARITY")); // Switch parity in x?
+    bool yFlip = (psMetadataLookupS32(NULL, target->concepts, "CELL.YPARITY") !=
+                  psMetadataLookupS32(NULL, source->concepts, "CELL.YPARITY")); // Switch parity in y?
+    psTrace(__func__, 3, "xFlip: %d; yFlip: %d\n", xFlip, yFlip);
+
+    bool mdok = true;                   // Status of MD lookup
+    psRegion *trimsec = psMetadataLookupPtr(&mdok, target->concepts, "CELL.TRIMSEC"); // The trim section
+    if (!mdok || !trimsec || psRegionIsBad(*trimsec)) {
+        psError(PS_ERR_IO, true, "CELL.TRIMSEC isn't set!\n");
+        return 0;
+    }
+    psList *biassecs = psMetadataLookupPtr(&mdok, target->concepts, "CELL.BIASSEC"); // The bias sections
+    if (!mdok || !biassecs) {
+        psError(PS_ERR_IO, true, "CELL.BIASSEC isn't set!\n");
+        return 0;
+    }
+
+    for (int i = 0; i < numReadouts; i++) {
+        pmReadout *sourceReadout = sourceReadouts->data[i]; // The source readout
+        psImage *sourceImage = sourceReadout->image; // The source image
+        pmReadout *targetReadout = pmReadoutAlloc(target); // The target readout; this adds it to the cell
+        if (sourceImage->numCols != trimsec->x1 - trimsec->x0 ||
+                sourceImage->numRows != trimsec->y1 - trimsec->y0) {
+            psString trimsecString = psRegionToString(*trimsec); // String with the trim section
+            psLogMsg(__func__, PS_LOG_WARN, "Source image size (%dx%d) for readout %d doesn't match "
+                     "CELL.TRIMSEC for target (%s) -- ignored.\n", sourceImage->numCols, sourceImage->numRows,
+                     i, trimsecString);
+            psFree(trimsecString);
+        } else {
+            binRegion(trimsec, xBin, yBin);
+            if (pixels) {
+                copyPixels(hdu->images->data[i], sourceImage, *trimsec, xFlip, yFlip);
+            }
+            targetReadout->image = psImageSubset(hdu->images->data[i], *trimsec);
+        }
+
+        psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
+        psRegion *biassec = NULL;       // Bias section from iteration
+        psListIterator *biasIter = psListIteratorAlloc(sourceReadout->bias, PS_LIST_HEAD, false); // Iterator
+        psImage *bias = NULL;           // Bias image from iteration
+        while ((biassec = psListGetAndIncrement(biassecsIter)) && (bias = psListGetAndIncrement(biasIter))) {
+            if (psRegionIsBad(*biassec)) {
+                psString biassecString = psRegionToString(*biassec); // String for bias section
+                psLogMsg(__func__, PS_LOG_WARN, "Bias section (%s) isn't set --- ignored.\n", biassecString);
+                psFree(biassecString);
+                continue;
+            }
+            if (bias->numCols != biassec->x1 - biassec->x0 ||
+                    bias->numRows != biassec->y1 - biassec->y0) {
+                psString biassecString = psRegionToString(*biassec); // String with the bias section
+                psLogMsg(__func__, PS_LOG_WARN, "Source image size (%dx%d) for readout %d doesn't match "
+                         "CELL.BIASSEC for target (%s) -- ignored.\n", bias->numCols, bias->numRows, i,
+                         biassecString);
+                psFree(biassecString);
+            } else {
+                binRegion(biassec, xBin, yBin);
+                if (pixels) {
+                    copyPixels(hdu->images->data[i], bias, *biassec, xFlip, yFlip);
+                }
+                psImage *newBias = psImageSubset(hdu->images->data[i], *biassec);
+                psListAdd(targetReadout->bias, PS_LIST_TAIL, newBias);
+                psFree(newBias);        // Drop reference
+            }
+        }
+        psFree(targetReadout);          // Drop reference
+        psFree(biassecsIter);
+        psFree(biasIter);
+    }
+
+    // Copy the remaining "concepts" over
+    psMetadataIterator *conceptsIter = psMetadataIteratorAlloc(source->concepts, PS_LIST_HEAD, NULL);
+    psMetadataItem *conceptItem = NULL; // Item from iteration
+    while ((conceptItem = psMetadataGetAndIncrement(conceptsIter))) {
+        psString name = conceptItem->name; // Name of concept
+        if (strcmp(name, "CELL.TRIMSEC") != 0 && strcmp(name, "CELL.BIASSEC") != 0 &&
+                strcmp(name, "CELL.XPARITY") != 0 && strcmp(name, "CELL.YPARITY") != 0 &&
+                strcmp(name, "CELL.X0") != 0 && strcmp(name, "CELL.Y0") != 0) {
+            psMetadataAddItem(target->concepts, conceptItem, PS_LIST_TAIL, PS_META_REPLACE);
+        }
+    }
+    psFree(conceptsIter);
+
+    // Need to update CELL.X0 and CELL.Y0 if we flipped
+    if (xFlip) {
+        int xZero = psMetadataLookupS32(NULL, source->concepts, "CELL.X0"); // CELL.X0 from source
+        int xParity = psMetadataLookupS32(NULL, target->concepts, "CELL.XPARITY"); // Parity in x
+        int xBin = psMetadataLookupS32(NULL, source->concepts, "CELL.XBIN"); // CELL.XBIN from source
+        pmReadout *readout = source->readouts->data[0]; // A representative readout
+        psTrace(__func__, 3, "CELL.X0: Before: %d After: %d\n", xZero,
+                xZero - (readout->image->numCols - 1) * xParity * xBin);
+        psTrace(__func__, 9, "(xParity: %d xBin: %d numCols: %d)\n", xParity, xBin, readout->image->numCols);
+        xZero -= (readout->image->numCols - 1) * xParity * xBin; // Change the parity on the X0 position
+        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.X0"); // CELL.X0 from target
+        newItem->data.S32 = xZero;
+    }
+    if (yFlip) {
+        int yZero = psMetadataLookupS32(NULL, source->concepts, "CELL.Y0"); // CELL.Y0 from source
+        int yParity = psMetadataLookupS32(NULL, target->concepts, "CELL.YPARITY"); // Parity in y
+        int yBin = psMetadataLookupS32(NULL, source->concepts, "CELL.YBIN"); // Parity in y
+        pmReadout *readout = source->readouts->data[0]; // A representative readout
+        psTrace(__func__, 3, "CELL.Y0: Before: %d After: %d\n", yZero,
+                yZero - (readout->image->numRows - 1) * yParity * yBin);
+        psTrace(__func__, 9, "(yParity: %d yBin: %d numRows: %d)\n", yParity, yBin, readout->image->numRows);
+        yZero -= (readout->image->numRows - 1) * yParity * yBin; // Change the parity on the Y0 position
+        psMetadataItem *newItem = psMetadataLookup(target->concepts, "CELL.Y0"); // CELL.Y0 from target
+        newItem->data.S32 = yZero;
+    }
+
+    // Update the binning concepts
+    psMetadataItem *binItem = psMetadataLookup(target->concepts, "CELL.XBIN");
+    binItem->data.S32 *= xBin;
+    binItem = psMetadataLookup(target->concepts, "CELL.YBIN");
+    binItem->data.S32 *= yBin;
+
+    return numReadouts;
+}
+
+static int chipCopy(pmChip *target,          // The target chip
+                    pmChip *source,          // The source chip, to be copied
+                    bool pixels,             // Copy the pixels?
+                    int xBin, int yBin       // (Relative) binning factors in x and y
+                   )
+{
+    assert(target);
+    assert(source);
+
+    psArray *targetCells = target->cells; // The target cells
+    psArray *sourceCells = source->cells; // The source cells
+    if (targetCells->n != sourceCells->n) {
+        psError(PS_ERR_IO, true, "Number of source cells (%d) differs from the number of target cells (%d)\n",
+                sourceCells->n, targetCells->n);
+        return false;
+    }
+
+    // Copy any headers
+    if (target->hdu && !target->hdu->phu) {
+        pmHDU *sourceHDU = pmHDUFromChip(source);
+        target->hdu->header = psMetadataCopy(target->hdu->header, sourceHDU->header);
+    }
+
+    int numCells = 0;                   // Number of cells copied
+    for (int i = 0; i < targetCells->n; i++) {
+        pmCell *targetCell = targetCells->data[i]; // The target cell
+        const char *cellName = psMetadataLookupStr(NULL, targetCell->concepts, "CELL.NAME"); // Name of cell
+        int cellNum = pmChipFindCell(source, cellName); // Number of cell with that name
+        if (cellNum >= 0) {
+            pmCell *sourceCell = sourceCells->data[cellNum]; // The source cell
+            int numReadouts = cellCopy(targetCell, sourceCell, pixels, xBin, yBin); // Number of readouts
+            // copied
+            psTrace(__func__, 5, "Copied %d readouts for cell %s\n", numReadouts, cellName);
+            numCells++;
+        }
+    }
+
+    // Update the concepts
+    psMetadataCopy(target->concepts, source->concepts);
+
+    return numCells;
+
+}
+
+static int fpaCopy(pmFPA *target,            // The target FPA
+                   pmFPA *source,            // The source FPA, to be copied
+                   bool pixels,              // Copy the pixels?
+                   int xBin, int yBin        // (Relative) binning factors in x and y
+                  )
+{
+    assert(target);
+    assert(source);
+
+    psArray *targetChips = target->chips; // The target chips
+    psArray *sourceChips = source->chips; // The source chips
+    if (targetChips->n != sourceChips->n) {
+        psError(PS_ERR_IO, true, "Number of source chips (%d) differs from the number of target chips (%d)\n",
+                sourceChips->n, targetChips->n);
+        return false;
+    }
+
+    // Copy any headers
+    if (target->hdu && !target->hdu->phu) {
+        pmHDU *sourceHDU = pmHDUFromFPA(source);
+        target->hdu->header = psMetadataCopy(target->hdu->header, sourceHDU->header);
+    }
+
+    int numChips = 0;                   // Number of chips copied
+    for (int i = 0; i < targetChips->n; i++) {
+        pmChip *targetChip = targetChips->data[i]; // The target chip
+        const char *chipName = psMetadataLookupStr(NULL, targetChip->concepts, "CHIP.NAME"); // Name of chip
+        int chipNum = pmFPAFindChip(source, chipName); // Number of chip with that name
+        if (chipNum >= 0) {
+            pmChip *sourceChip = sourceChips->data[chipNum]; // The source chip
+            int numCells = chipCopy(targetChip, sourceChip, pixels, xBin, yBin); // Number of cells copied
+            psTrace(__func__, 5, "Copied %d cells for chip %s\n", numCells, chipName);
+            numChips++;
+        }
+    }
+
+    // Update the concepts
+    psMetadataCopy(target->concepts, source->concepts);
+
+    return numChips;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+int pmFPACopy(pmFPA *target,            // The target FPA
+              pmFPA *source             // The source FPA, to be copied
+             )
+{
+    return fpaCopy(target, source, true, 1, 1);
+}
+
+int pmChipCopy(pmChip *target,          // The target chip
+               pmChip *source           // The source chip, to be copied
+              )
+{
+    return chipCopy(target, source, true, 1, 1);
+}
+
+int pmCellCopy(pmCell *target,          // The target cell
+               pmCell *source           // The source cell, to be copied
+              )
+{
+    return cellCopy(target, source, true, 1, 1);
+}
+
+
+int pmFPACopyStructure(pmFPA *target,   // The target FPA
+                       pmFPA *source,   // The source FPA, to be copied
+                       int xBin, int yBin // Binning factors in x and y
+                      )
+{
+    return fpaCopy(target, source, false, xBin, yBin);
+}
+
+int pmChipCopyStructure(pmChip *target, // The target chip
+                        pmChip *source, // The source chip, to be copied
+                        int xBin, int yBin // Binning factors in x and y
+                       )
+{
+    return chipCopy(target, source, false, xBin, yBin);
+}
+
+int pmCellCopyStructure(pmCell *target, // The target cell
+                        pmCell *source, // The source cell, to be copied
+                        int xBin, int yBin // Binning factors in x and y
+                       )
+{
+    return cellCopy(target, source, false, xBin, yBin);
+}
Index: trunk/psModules/src/astrom/pmFPACopy.h
===================================================================
--- trunk/psModules/src/astrom/pmFPACopy.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPACopy.h	(revision 6872)
@@ -0,0 +1,30 @@
+#ifndef PM_FPA_COPY_H
+#define PM_FPA_COPY_H
+
+// Copy the FPA components, including the pixels
+int pmFPACopy(pmFPA *target,            // The target FPA
+              pmFPA *source             // The source FPA, to be copied
+             );
+int pmChipCopy(pmChip *target,          // The target chip
+               pmChip *source           // The source chip, to be copied
+              );
+int pmCellCopy(pmCell *target,          // The target cell
+               pmCell *source           // The source cell, to be copied
+              );
+
+// Versions that copy the structure and not the pixels; they also allow binning
+int pmFPACopyStructure(pmFPA *target,   // The target FPA
+                       pmFPA *source,   // The source FPA, to be copied
+                       int xBin, int yBin     // Binning factors in x and y
+                      );
+int pmChipCopyStructure(pmChip *target, // The target chip
+                        pmChip *source, // The source chip, to be copied
+                        int xBin, int yBin   // Binning factors in x and y
+                       );
+int pmCellCopyStructure(pmCell *target, // The target cell
+                        pmCell *source, // The source cell, to be copied
+                        int xBin, int yBin // Binning factors in x and y
+                       );
+
+
+#endif
Index: trunk/psModules/src/astrom/pmFPAMaskWeight.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAMaskWeight.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAMaskWeight.c	(revision 6872)
@@ -0,0 +1,101 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmFPAMaskWeight.h"
+
+bool pmReadoutSetMask(pmReadout *readout // Readout for which to set mask
+                     )
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Get the "concepts" of interest
+    float saturation = psMetadataLookupF32(&mdok, cell->concepts, "CELL.SATURATION"); // Saturation level
+    if (!mdok || isnan(saturation)) {
+        psError(PS_ERR_IO, true, "CELL.SATURATION is not set --- unable to set mask.\n");
+        return false;
+    }
+    float bad = psMetadataLookupF32(NULL, cell->concepts, "CELL.BAD"); // Bad level
+    if (!mdok || isnan(bad)) {
+        psError(PS_ERR_IO, true, "CELL.BAD is not set --- unable to set mask.\n");
+        return false;
+    }
+
+    psImage *image = readout->image;    // The image pixels
+    psImage *mask = readout->mask;      // The mask pixels
+    if (!mask) {
+        mask = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+        readout->mask = mask;
+        psImageInit(mask, 0);
+    }
+
+    // Set up the mask
+    for (int i = 0; i < image->numRows; i++) {
+        for (int j = 0; j < image->numCols; j++) {
+            if (image->data.F32[i][j] > saturation) {
+                mask->data.U8[i][j] |= PM_MASK_SAT;
+            }
+            if (image->data.F32[i][j] < bad) {
+                mask->data.U8[i][j] |= PM_MASK_BAD;
+            }
+        }
+    }
+
+    return true;
+}
+
+bool pmReadoutSetWeight(pmReadout *readout // Readout for which to set weight
+                       )
+{
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+
+    pmCell *cell = readout->parent;     // The parent cell
+    bool mdok = true;                   // Status of MD lookup
+
+    // Get the "concepts" of interest
+    float gain = psMetadataLookupF32(&mdok, cell->concepts, "CELL.GAIN"); // Cell gain
+    if (!mdok || isnan(gain)) {
+        psError(PS_ERR_IO, true, "CELL.GAIN is not set --- unable to set weight.\n");
+        return false;
+    }
+    float readnoise = psMetadataLookupF32(&mdok, cell->concepts, "CELL.READNOISE"); // Cell read noise
+    if (!mdok || isnan(readnoise)) {
+        psError(PS_ERR_IO, true, "CELL.READNOISE is not set --- unable to set weight.\n");
+        return false;
+    }
+
+    psImage *image = readout->image;    // The image pixels
+    psImage *weight = readout->weight;  // The weight pixels
+    if (!weight) {
+        weight = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+        readout->weight = weight;
+        psImageInit(weight, 0.0);
+    }
+
+    // Set weight image to the variance in ADU = f/g + rn^2
+    psBinaryOp(readout->weight, image, "/", psScalarAlloc(gain, PS_TYPE_F32));
+    psBinaryOp(readout->weight, readout->weight, "+",
+               psScalarAlloc(readnoise*readnoise/gain/gain, PS_TYPE_F32));
+
+    return true;
+}
+
+
+
+bool pmCellSetMaskWeight(pmCell *cell // Cell for which to set weights
+                        )
+{
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+
+    bool success = true;                // Was everything successful?
+    psArray *readouts = cell->readouts; // Array of readouts
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout
+        success |= pmReadoutSetMask(readout);
+        success |= pmReadoutSetWeight(readout);
+    }
+
+    return success;
+}
Index: trunk/psModules/src/astrom/pmFPAMaskWeight.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAMaskWeight.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAMaskWeight.h	(revision 6872)
@@ -0,0 +1,23 @@
+#ifndef PM_READOUT_MASK_WEIGHT_H
+#define PM_READOUT_MASK_WEIGHT_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_BAD     = 0x0008,   ///< The pixel is low
+    PM_MASK_FLAT    = 0x0010    ///< The pixel is non-positive in the flat-field.
+} pmMaskValue;
+
+bool pmReadoutSetMask(pmReadout *readout // Readout for which to set mask
+                     );
+bool pmReadoutSetWeight(pmReadout *readout // Readout for which to set weight
+                       );
+bool pmCellSetMaskWeight(pmCell *cell // Cell for which to set weights
+                        );
+
+
+#endif
Index: trunk/psModules/src/astrom/pmFPARead.c
===================================================================
--- trunk/psModules/src/astrom/pmFPARead.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPARead.c	(revision 6872)
@@ -0,0 +1,242 @@
+#include <stdio.h>
+#include <strings.h>
+#include <assert.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmFPARead.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmConcepts.h"
+#include "psRegionIsBad.h"
+#include "pmHDUUtils.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Carve a readout from the image pixels
+static pmReadout *readoutCarve(pmCell *cell, // Cell into which the new readout will be placed
+                               psImage *image, // Image that will be carved
+                               const psRegion *trimsec, // Trim section
+                               const psList *biassecs // Bias sections
+                              )
+{
+    pmReadout *readout = pmReadoutAlloc(cell);
+
+    // The image corresponding to the trim region
+    if (psRegionIsBad(*trimsec)) {
+        psString regionString = psRegionToString(*trimsec);
+        psError(PS_ERR_IO, true, "Invalid trim section: %s\n", regionString);
+        psFree(regionString);
+        psFree(readout);
+        return NULL;
+    }
+    readout->image = psMemIncrRefCounter(psImageSubset(image, *trimsec));
+
+    // Get the list of overscans
+    psListIterator *iter = psListIteratorAlloc((psList*)biassecs, PS_LIST_HEAD, false); // Iterator
+    psRegion *biassec = NULL;       // A BIASSEC region from the list
+    while ((biassec = psListGetAndIncrement(iter))) {
+        if (psRegionIsBad(*biassec)) {
+            psString regionString = psRegionToString(*biassec);
+            psError(PS_ERR_IO, true, "Invalid bias section: %s\n", regionString);
+            psFree(regionString);
+            psFree(readout);
+            return NULL;
+        }
+        psImage *overscan = psMemIncrRefCounter(psImageSubset(image, *biassec));
+        psListAdd(readout->bias, PS_LIST_TAIL, overscan);
+        psFree(overscan);
+    }
+    psFree(iter);
+
+    return readout;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Read the next readout; return true if we read pixels in.
+bool pmReadoutReadNext(pmReadout *readout, // Readout into which to read
+                       psFits *fits,    // FITS file from which to read
+                       int z,           // Readout number/plane; zero-offset indexing
+                       int numRows      // The number of rows to read
+                      )
+{
+    // Get the HDU and read the header
+    pmCell *cell = readout->parent;     // The parent cell
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    if (!hdu) {
+        return false;
+    }
+
+    if (!pmHDUReadHeader(hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read HDU header!\n");
+        return false;
+    }
+
+    // Check the third dimension
+    bool mdok = true;                   // Status of MD lookup
+    int naxis = psMetadataLookupS32(&mdok, hdu->header, "NAXIS"); // The number of axes
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS in header for extension %s\n", hdu->extname);
+        return false;
+    }
+    if (naxis == 0) {
+        // No pixels to read, as for a PHU.
+        return false;
+    }
+    if (naxis < 2 || naxis > 3) {
+        psError(PS_ERR_IO, true, "NAXIS in header of extension %s (= %d) is not valid.\n",
+                hdu->extname, naxis);
+        return false;
+    }
+    int naxis3 = 1;                     // The number of image planes
+    if (naxis == 3) {
+        naxis3 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS3");
+        if (!mdok) {
+            psError(PS_ERR_IO, true, "Unable to find NAXIS3 in header for extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+    if (z >= naxis3) {
+        // Nothing to see here.  Move along.
+        return false;
+    }
+
+    // Get the size of the image plane
+    int naxis1 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS1"); // The number of columns
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS1 in header for extension %s\n", hdu->extname);
+        return false;
+    }
+    int naxis2 = psMetadataLookupS32(&mdok, hdu->header, "NAXIS2"); // The number of columns
+    if (!mdok) {
+        psError(PS_ERR_IO, true, "Unable to find NAXIS2 in header for extension %s\n", hdu->extname);
+        return false;
+    }
+
+    // Read the FITS image
+    int offset = readout->row0;         // The row number to read
+    if (readout->image) {
+        offset += readout->image->numRows;
+    }
+    if (offset >= naxis2) {
+        // We've read everything there is
+        return false;
+    }
+    int upper = offset + numRows;       // The upper limit for the pixel read
+    if (upper > naxis2) {
+        upper = naxis2;
+    }
+    psRegion region = psRegionSet(0, naxis1, offset, upper); // Region to be read
+    psImage *image = psFitsReadImage(fits, region, z); // The image
+    if (readout->image) {
+        psFree(readout->image);
+    }
+
+    // Stick the image into the HDU and the readout
+    if (!hdu->images) {
+        hdu->images = psArrayAlloc(naxis3);
+    }
+    if (hdu->images->n != naxis3) {
+        hdu->images = psArrayRealloc(hdu->images, naxis3);
+    }
+    if (hdu->images->data[z]) {
+        psFree(hdu->images->data[z]);
+    }
+    hdu->images->data[z] = image;
+    readout->image = psImageSubset(hdu->images->data[z], psRegionSet(0,0,0,0));
+    readout->row0 = region.y0;
+
+    return true;
+}
+
+
+// Read in the cell, and allocate the readouts
+bool pmCellRead(pmCell *cell,           // Cell to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               )
+{
+    pmHDU *hdu = pmHDUFromCell(cell);   // The HDU
+    if (!hdu) {
+        return false;                    // Nothing to see here; move along
+    }
+    if (!hdu->images && !pmHDURead(hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to read HDU for cell.\n");
+        return false;
+    }
+
+    pmConceptsReadCell(cell, PM_CONCEPT_SOURCE_HEADER, false, NULL);
+
+    // Having read the cell, we now have to cut it up
+    psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC");
+    psList *biassecs = psMetadataLookupPtr(NULL, cell->concepts, "CELL.BIASSEC");
+
+    // Iterate over each of the image planes
+    for (int i = 0; i < hdu->images->n; i++) {
+        psImage *image = hdu->images->data[i]; // The i-th plane
+
+        // XXX: Type conversion here to support the modules, which don't have multiple type support yet
+        if (image->type.type != PS_TYPE_F32) {
+            psImage *temp = psImageCopy(NULL, image, PS_TYPE_F32); // Temporary image
+            psFree(hdu->images->data[i]);
+            hdu->images->data[i] = temp;
+            image = temp;
+        }
+
+        pmReadout *readout = readoutCarve(cell, image, trimsec, biassecs);
+        readout->mask = NULL;
+        readout->weight = NULL;
+        psFree(readout);                // Drop reference
+    }
+
+    pmCellSetDataStatus (cell, true);
+    return true;
+}
+
+// Read in the component cells
+bool pmChipRead(pmChip *chip,           // Chip to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               )
+{
+    bool success = false;               // Were we able to read at least one HDU?
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        success |= pmCellRead(cell, fits, db);
+    }
+    if (success) {
+        pmConceptsReadChip(chip, PM_CONCEPT_SOURCE_HEADER, false, NULL);
+        // XXX probably could just use chip->data_exists
+        pmChipSetDataStatus (chip, true);
+    }
+
+    return success;
+}
+
+// Read in the component chips
+bool pmFPARead(pmFPA *fpa,              // FPA to read into
+               psFits *fits,            // FITS file from which to read
+               psDB *db                 // Database handle, for "concepts" ingest
+              )
+{
+    bool success = false;               // Were we able to read at least one HDU?
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The cell of interest
+        success |= pmChipRead(chip, fits, db);
+    }
+    if (success) {
+        pmConceptsReadFPA(fpa, PM_CONCEPT_SOURCE_HEADER, NULL);
+    } else {
+        psLogMsg(__func__, PS_LOG_WARN, "Unable to read any chips in FPA.\n");
+    }
+
+    return success;
+}
+
Index: trunk/psModules/src/astrom/pmFPARead.h
===================================================================
--- trunk/psModules/src/astrom/pmFPARead.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPARead.h	(revision 6872)
@@ -0,0 +1,67 @@
+#ifndef PM_FPA_READ_H
+#define PM_FPA_READ_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+
+bool pmReadoutReadNext(pmReadout *readout, // Readout into which to read
+                       psFits *fits,    // FITS file from which to read
+                       int z,           // Readout number/plane; zero-offset indexing
+                       int numRows      // The number of rows to read
+                      );
+
+bool pmCellRead(pmCell *cell,           // Cell to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               );
+
+bool pmChipRead(pmChip *chip,           // Chip to read into
+                psFits *fits,           // FITS file from which to read
+                psDB *db                // Database handle, for "concepts" ingest
+               );
+
+bool pmFPARead(pmFPA *fpa,              // FPA to read into
+               psFits *fits,            // FITS file from which to read
+               psDB *db                 // Database handle, for "concepts" ingest
+              );
+
+bool pmCellReadPHU(pmCell *cell,        // Cell to read into
+                   psFits *fits         // FITS file from which to read
+                  );
+
+bool pmChipReadPHU(pmChip *chip,        // Chip to read into
+                   psFits *fits         // FITS file from which to read
+                  );
+
+bool pmFPAReadPHU(pmFPA *fpa,           // FPA to read into
+                  psFits *fits          // FITS file from which to read
+                 );
+
+
+#if 0
+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(const psString name, // The name to translate
+                              const pmCell *cell // The cell for which to translate
+                             );
+
+psString p_pmFPATranslateFileExt(psString *extName, // Extension name, to be returned
+                                 const psString filenameExt, // The string to parse into filename and ext
+                                 const pmCell *cell // The cell
+                                );
+
+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
+
+#endif
Index: trunk/psModules/src/astrom/pmFPAUtils.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAUtils.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAUtils.c	(revision 6872)
@@ -0,0 +1,41 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmFPAUtils.h"
+
+// Find a chip by name; return the index
+int pmFPAFindChip(const pmFPA *fpa, // FPA in which to find the chip
+                  const char *name // Name of the chip
+                 )
+{
+    psArray *chips = fpa->chips;    // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i]; // The chip of interest
+        psString testName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME"); // Name of this chip
+        if (strcmp(name, testName) == 0) {
+            return i;
+        }
+    }
+
+    psError(PS_ERR_IO, true, "Unable to find chip %s\n", name);
+    return -1;
+}
+
+// Find a cell by name; return the index
+int pmChipFindCell(const pmChip *chip, // Chip in which to find the cell
+                   const char *name // Name of the cell
+                  )
+{
+    psArray *cells = chip->cells;    // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i]; // The cell of interest
+        psString testName = psMetadataLookupStr(NULL, cell->concepts, "CELL.NAME"); // Name of this cell
+        if (strcmp(name, testName) == 0) {
+            return i;
+        }
+    }
+
+    psError(PS_ERR_IO, true, "Unable to find cell %s\n", name);
+    return -1;
+}
+
Index: trunk/psModules/src/astrom/pmFPAUtils.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAUtils.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAUtils.h	(revision 6872)
@@ -0,0 +1,17 @@
+#ifndef PM_FPA_UTILS_H
+#define PM_FPA_UTILS_H
+
+#include "pmFPA.h"
+
+// Find a chip by name; return the index
+int pmFPAFindChip(const pmFPA *fpa, // FPA in which to find the chip
+                  const char *name // Name of the chip
+                 );
+
+// Find a cell by name; return the index
+int pmChipFindCell(const pmChip *chip, // Chip in which to find the cell
+                   const char *name // Name of the cell
+                  );
+
+
+#endif
Index: trunk/psModules/src/astrom/pmFPAWrite.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAWrite.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAWrite.c	(revision 6872)
@@ -0,0 +1,78 @@
+#include <stdio.h>
+#include <strings.h>
+#include "pslib.h"
+
+#include "pmFPA.h"
+#include "pmHDU.h"
+#include "pmConcepts.h"
+
+
+bool pmCellWrite(pmCell *cell,          // Cell to write
+                 psFits *fits,          // FITS file to which to write
+                 psDB *db               // Database handle for "concepts" update
+                )
+{
+    pmConceptsWriteCell(cell, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CAMERA | PM_CONCEPT_SOURCE_DEFAULTS,
+                        false, NULL);
+
+    pmHDU *hdu = cell->hdu;             // The HDU
+    if (hdu && !pmHDUWrite(hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to write HDU for Chip.\n");
+        return false;
+    }
+
+    return true;
+}
+
+
+bool pmChipWrite(pmChip *chip,          // Chip to write
+                 psFits *fits,          // FITS file to which to write
+                 psDB *db               // Database handle for "concepts" update
+                )
+{
+    pmConceptsWriteChip(chip, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CAMERA | PM_CONCEPT_SOURCE_DEFAULTS,
+                        false, NULL);
+
+    pmHDU *hdu = chip->hdu;             // The HDU
+    if (hdu && !pmHDUWrite(hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to write HDU for Chip.\n");
+        return false;
+    }
+
+    bool success = true;                // Success of writing
+    psArray *cells = chip->cells;       // Array of cells
+    for (int i = 0; i < cells->n; i++) {
+        pmCell *cell = cells->data[i];  // The cell of interest
+        success |= pmCellWrite(cell, fits, db);
+    }
+
+    return success;
+}
+
+
+
+
+bool pmFPAWrite(pmFPA *fpa,             // FPA to write
+                psFits *fits,           // FITS file to which to write
+                psDB *db                // Database handle for "concepts" update
+               )
+{
+    pmConceptsWriteFPA(fpa, PM_CONCEPT_SOURCE_HEADER | PM_CONCEPT_SOURCE_CAMERA | PM_CONCEPT_SOURCE_DEFAULTS,
+                       NULL);
+
+    pmHDU *hdu = fpa->hdu;              // The HDU
+    if (hdu && !pmHDUWrite(hdu, fits)) {
+        psError(PS_ERR_IO, false, "Unable to write HDU for FPA.\n");
+        return false;
+    }
+
+    bool success = true;                // Success of writing
+    psArray *chips = fpa->chips;        // Array of chips
+    for (int i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // The chip of interest
+        success |= pmChipWrite(chip, fits, db);
+    }
+
+    return success;
+}
+
Index: trunk/psModules/src/astrom/pmFPAWrite.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAWrite.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAWrite.h	(revision 6872)
@@ -0,0 +1,21 @@
+#ifndef PM_FPA_WRITE_H
+#define PM_FPA_WRITE_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+
+bool pmCellWrite(pmCell *cell,          // Cell to write
+                 psFits *fits,          // FITS file to which to write
+                 psDB *db               // Database handle for "concepts" update
+                );
+bool pmChipWrite(pmChip *chip,          // Chip to write
+                 psFits *fits,          // FITS file to which to write
+                 psDB *db               // Database handle for "concepts" update
+                );
+bool pmFPAWrite(pmFPA *fpa,             // FPA to write
+                psFits *fits,           // FITS file to which to write
+                psDB *db                // Database handle for "concepts" update
+               );
+
+#endif
Index: trunk/psModules/src/astrom/pmFPA_JPEG.c
===================================================================
--- trunk/psModules/src/astrom/pmFPA_JPEG.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPA_JPEG.c	(revision 6872)
@@ -0,0 +1,163 @@
+/** @file  pmFPA_JPEG.c
+ *
+ * This file contains functions to write JPEG images.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:04 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <pslib.h>
+# include "psImageJpeg.h"
+# include "psImageUnbin.h"
+# include "psAdditionals.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAfile.h"
+#include "pmFPAview.h"
+#include "pmFPA_JPEG.h"
+
+
+bool pmFPAviewWriteJPEG (const pmFPAview *view, pmFPAfile *file)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWriteJPEG (fpa, view, file);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipWriteJPEG (chip, view, file);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWriteJPEG (cell, view, file);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutWriteJPEG (readout, view, file);
+    return true;
+}
+
+// read in all chip-level JPEG files for this FPA
+bool pmFPAWriteJPEG (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipWriteJPEG (chip, view, file);
+    }
+    return true;
+}
+
+// read in all cell-level JPEG files for this chip
+bool pmChipWriteJPEG (pmChip *chip, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellWriteJPEG (cell, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level JPEG files for this cell
+bool pmCellWriteJPEG (pmCell *cell, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutWriteJPEG (readout, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutWriteJPEG (pmReadout *readout, const pmFPAview *view, pmFPAfile *file)
+{
+    char *name, *mode, *word, *mapname;
+    psArray *range;
+    psImageJpegColormap *map;
+
+    switch (file->type) {
+    case PM_FPA_FILE_JPEG:
+
+        name = pmFPAfileNameFromRule (file->filerule, file, view);
+
+        mapname = pmFPAfileNameFromRule (file->filextra, file, view);
+        map = psImageJpegColormapSet (NULL, mapname);
+        if (!map) {
+            map = psImageJpegColormapSet (NULL, "-greyscale");
+        }
+
+        mode = pmFPAfileNameFromRule (file->extrule, file, view);
+        word = pmFPAfileNameFromRule (file->extxtra, file, view);
+        range = psStringSplitArray (word, ":");
+
+        // XXX we need to decide where the scale will come from in the long term
+        psImageClippedStatsInit (10000);
+
+        psStats *stats = psImageClippedStats (readout->image, NULL, 0, 0.25, 0.75);
+        float delta = stats->robustUQ - stats->robustLQ;
+        float min = stats->robustMedian - 3*delta;
+        float max = stats->robustMedian - 6*delta;
+        if (!strcasecmp (mode, "RANGE") && range) {
+            float fmin = atof(range->data[0]);
+            float fmax = atof(range->data[1]);
+            min = stats->robustMedian + fmin*delta;
+            max = stats->robustMedian + fmax*delta;
+        }
+        if (!strcasecmp (mode, "FRACTION")) {
+            float fmin = atof(range->data[0]);
+            float fmax = atof(range->data[1]);
+            min = fmin*stats->robustMedian;
+            max = fmax*stats->robustMedian;
+        }
+        psImageJpeg (map, readout->image, name, min, max);
+
+        psFree (name);
+        psFree (mode);
+        psFree (word);
+        psFree (mapname);
+        psFree (map);
+        psFree (stats);
+        psImageClippedStatsCleanup ();
+
+        return true;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    return false;
+}
Index: trunk/psModules/src/astrom/pmFPA_JPEG.h
===================================================================
--- trunk/psModules/src/astrom/pmFPA_JPEG.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPA_JPEG.h	(revision 6872)
@@ -0,0 +1,27 @@
+/** @file  pmFPAview.h
+*
+*  @brief Tools to manipulate the FPA structure elements.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
+*
+*  Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+*/
+
+#ifndef PM_FPA_JPEG_H
+#define PM_FPA_JPEG_H
+
+/// @addtogroup AstroImage
+/// @{
+
+bool pmFPAviewWriteJPEG (const pmFPAview *view, pmFPAfile *file);
+bool pmFPAWriteJPEG (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file);
+bool pmChipWriteJPEG (pmChip *chip, const pmFPAview *view, pmFPAfile *file);
+bool pmCellWriteJPEG (pmCell *cell, const pmFPAview *view, pmFPAfile *file);
+bool pmReadoutWriteJPEG (pmReadout *readout, const pmFPAview *view, pmFPAfile *file);
+
+# endif
Index: trunk/psModules/src/astrom/pmFPAfile.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAfile.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAfile.c	(revision 6872)
@@ -0,0 +1,1092 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "psAdditionals.h"
+#include "pmConfig.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAConstruct.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPACopy.h"
+#include "pmFPARead.h"
+#include "pmFPAWrite.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceIO.h"
+#include "pmGrowthCurve.h"
+#include "pmPSF.h"
+#include "pmPSF_IO.h"
+#include "pmFPA_JPEG.h"
+
+static void pmFPAfileFree (pmFPAfile *file)
+{
+
+    if (file == NULL)
+        return;
+
+    psFree (file->fpa);
+    psFree (file->readout);
+    psFree (file->names);
+
+    psFree (file->format);
+    psFree (file->name);
+
+    if (file->fits != NULL) {
+        psFitsClose (file->fits);
+    }
+
+    psFree (file->filerule);
+    psFree (file->filextra);
+    psFree (file->extrule);
+    psFree (file->extxtra);
+
+    psFree (file->filename);
+    psFree (file->extname);
+
+    // these are just views ??
+    // psFree (file->phu);
+    // psFree (file->header);
+
+    return;
+}
+
+pmFPAfile *pmFPAfileAlloc ()
+{
+
+    pmFPAfile *file = psAlloc (sizeof(pmFPAfile));
+    psMemSetDeallocator (file, (psFreeFunc) pmFPAfileFree);
+
+    file->phu = NULL;
+    file->readout = NULL;
+    file->header = NULL;
+
+    file->fpa = NULL;
+    file->fits = NULL;
+    file->names = psMetadataAlloc();
+
+    file->format = NULL;
+    file->name = NULL;
+
+    file->filerule = NULL;
+    file->filextra = NULL;
+    file->extrule  = NULL;
+    file->extxtra  = NULL;
+
+    file->filename = NULL;
+    file->extname  = NULL;
+
+    file->type = PM_FPA_FILE_NONE;
+    file->mode = PM_FPA_MODE_NONE;
+    file->state = PM_FPA_STATE_CLOSED;
+
+    return (file);
+}
+
+pmFPAfile *pmFPAfileDefine (psMetadata *files, psMetadata *camera, pmFPA *fpa, char *name)
+{
+
+    bool status;
+    char *depth;
+    char *type;
+
+    // select the FILERULES from the camera config
+    psMetadata *filerules = psMetadataLookupPtr (&status, camera, "FILERULES");
+    if (filerules == NULL) {
+        psErrorStackPrint(stderr, "Can't find FILERULES in the CAMERA configuration!\n");
+        return NULL;
+    }
+
+    // select the name from the FILERULES
+    // check for alias name (type == STR, name is aliased name)
+    char *realname = psMetadataLookupStr (&status, filerules, name);
+    if (realname == NULL)
+        realname = name;
+
+    psMetadata *data = psMetadataLookupPtr (&status, filerules, realname);
+    if (data == NULL) {
+        psErrorStackPrint(stderr, "Can't find file concept %s!\n", name);
+        return NULL;
+    }
+
+    pmFPAfile *file = pmFPAfileAlloc ();
+
+    // save the name of this pmFPAfile
+    file->name = psStringCopy (name);
+
+    file->filerule = psMemIncrRefCounter(psMetadataLookupStr (&status, data, "FILENAME.RULE"));
+    file->filextra = psMemIncrRefCounter(psMetadataLookupStr (&status, data, "FILENAME.XTRA"));
+    file->extrule  = psMemIncrRefCounter(psMetadataLookupStr (&status, data, "EXTNAME.RULE"));
+    file->extxtra  = psMemIncrRefCounter(psMetadataLookupStr (&status, data, "EXTNAME.XTRA"));
+
+    file->fileDepth = PM_FPA_DEPTH_NONE;
+    depth = psMetadataLookupStr (&status, data, "FILE.DEPTH");
+    if (depth != NULL) {
+        if (!strcasecmp (depth, "FPA"))     {
+            file->fileDepth = PM_FPA_DEPTH_FPA;
+        }
+        if (!strcasecmp (depth, "CHIP"))    {
+            file->fileDepth = PM_FPA_DEPTH_CHIP;
+        }
+        if (!strcasecmp (depth, "CELL"))    {
+            file->fileDepth = PM_FPA_DEPTH_CELL;
+        }
+        if (!strcasecmp (depth, "READOUT")) {
+            file->fileDepth = PM_FPA_DEPTH_READOUT;
+        }
+    }
+    if (file->fileDepth == PM_FPA_DEPTH_NONE) {
+        psLogMsg (__func__, 3, "warning: FILE.DEPTH is not set for %s\n", name);
+    }
+
+    file->dataDepth = PM_FPA_DEPTH_NONE;
+    depth = psMetadataLookupStr (&status, data, "DATA.DEPTH");
+    if (depth != NULL) {
+        if (!strcasecmp (depth, "FPA"))     {
+            file->dataDepth = PM_FPA_DEPTH_FPA;
+        }
+        if (!strcasecmp (depth, "CHIP"))    {
+            file->dataDepth = PM_FPA_DEPTH_CHIP;
+        }
+        if (!strcasecmp (depth, "CELL"))    {
+            file->dataDepth = PM_FPA_DEPTH_CELL;
+        }
+        if (!strcasecmp (depth, "READOUT")) {
+            file->dataDepth = PM_FPA_DEPTH_READOUT;
+        }
+    }
+    if (file->dataDepth == PM_FPA_DEPTH_NONE) {
+        psLogMsg (__func__, 3, "warning: DATA.DEPTH is not set for %s\n", name);
+    }
+
+    file->type = PM_FPA_FILE_NONE;
+    type = psMetadataLookupStr (&status, data, "FILE.TYPE");
+    if (type != NULL) {
+        if (!strcasecmp (type, "SX"))     {
+            file->type = PM_FPA_FILE_SX;
+        }
+        if (!strcasecmp (type, "OBJ"))     {
+            file->type = PM_FPA_FILE_OBJ;
+        }
+        if (!strcasecmp (type, "CMP"))     {
+            file->type = PM_FPA_FILE_CMP;
+        }
+        if (!strcasecmp (type, "CMF"))     {
+            file->type = PM_FPA_FILE_CMF;
+        }
+        if (!strcasecmp (type, "RAW"))     {
+            file->type = PM_FPA_FILE_RAW;
+        }
+        if (!strcasecmp (type, "IMAGE"))     {
+            file->type = PM_FPA_FILE_IMAGE;
+        }
+        if (!strcasecmp (type, "PSF"))     {
+            file->type = PM_FPA_FILE_PSF;
+        }
+        if (!strcasecmp (type, "JPEG"))     {
+            file->type = PM_FPA_FILE_JPEG;
+        }
+    }
+    if (file->type == PM_FPA_FILE_NONE) {
+        psLogMsg (__func__, 3, "warning: FILE.TYPE is not defined for %s\n", name);
+    }
+
+    file->mode = PM_FPA_MODE_NONE;
+    type = psMetadataLookupStr (&status, data, "FILE.MODE");
+    if (type != NULL) {
+        if (!strcasecmp (type, "READ"))     {
+            file->mode = PM_FPA_MODE_READ;
+        }
+        if (!strcasecmp (type, "WRITE"))     {
+            file->mode = PM_FPA_MODE_WRITE;
+        }
+    }
+    if (file->mode == PM_FPA_MODE_NONE) {
+        psLogMsg (__func__, 3, "warning: FILE.MODE is not defined for %s\n", name);
+    }
+
+    if (fpa != NULL) {
+        file->fpa = psMemIncrRefCounter(fpa);
+    }
+
+    // for WRITE type of data, the output format needs to be determined
+    // this is only needed if the output file is not identical in structure to the input
+    char *formatName = psMetadataLookupStr (&status, data, "FILE.FORMAT");
+    if (formatName && strcasecmp (formatName, "NONE")) {
+        psMetadata *formats = psMetadataLookupMD(&status, camera, "FORMATS"); // List of formats
+        char *formatFile = psMetadataLookupStr (&status, formats, formatName);
+        readConfig (&file->format, formatFile, formatName);
+    }
+
+    psMetadataAddPtr (files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN, "", file);
+
+    // we free this copy of file, but 'files' still has a copy
+    psFree (file);
+
+    // the returned value is a view into the version on 'files'
+    return (file);
+}
+
+/*
+pmFPAfile *pmFPAfileConstruct (psMetadata *files, psMetadata *format, psMetadata *camera, char *name)
+{
+    pmFPA *fpa = pmFPAConstruct (camera);
+    pmFPAfile *file = pmFPAfileDefine (files, format, fpa, name);
+    psFree (fpa);
+    return file;
+}
+*/
+
+// open file (if not already opened)
+bool pmFPAfileOpen (pmFPAfile *file, const pmFPAview *view)
+{
+
+    bool status;
+    char *extra;
+    char *mode = NULL;
+    char *readMode = "r";
+    char *writeMode = "w";
+
+    if (file->mode == PM_FPA_MODE_NONE) {
+        return false;
+    }
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        return false;
+    }
+    if (file->mode == PM_FPA_MODE_READ) {
+        mode = readMode;
+    }
+    if (file->mode == PM_FPA_MODE_WRITE) {
+        mode = writeMode;
+    }
+
+    if (file->state == PM_FPA_STATE_OPEN) {
+        return true;
+    }
+
+    // determine the file name
+    file->filename = pmFPAfileNameFromRule (file->filerule, file, view);
+    if (file->filename == NULL)
+        return false;
+
+    // indirect filenames
+    if (!strcasecmp (file->filename, "@FILES")) {
+        psFree (file->filename);
+        extra = pmFPAfileNameFromRule (file->filextra, file, view);
+        file->filename = psMetadataLookupStr (&status, file->names, extra);
+        psFree (extra);
+        if (file->filename == NULL)
+            return false;
+        // psMetadataLookupStr just returns a view, file->filename must be protected
+        psMemIncrRefCounter (file->filename);
+    }
+    if (!strcasecmp (file->filename, "@DETDB")) {
+        psFree (file->filename);
+        extra = pmFPAfileNameFromRule (file->filextra, file, view);
+        // file->filename = pmDetrendSelect (extra);
+        psFree (extra);
+        if (file->filename == NULL)
+            return false;
+        psMemIncrRefCounter (file->filename);
+    }
+
+    switch (file->type) {
+        // open the FITS types:
+    case PM_FPA_FILE_IMAGE:
+    case PM_FPA_FILE_CMF:
+        psTrace ("pmFPAfile", 5, "opening %s (type: %d)\n", file->filename, file->type);
+        file->fits = psFitsOpen (file->filename, mode);
+        if (file->fits == NULL)
+            psAbort (__func__, "error opening file %s\n", file->filename);
+        file->state = PM_FPA_STATE_OPEN;
+        break;
+
+        // defer opening TEXT types:
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_PSF:
+    case PM_FPA_FILE_JPEG:
+        psTrace ("pmFPAfile", 5, "not opening %s (type: %d)\n", file->filename, file->type);
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch for %s : %d\n", file->filename, file->type);
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAfileRead (pmFPAfile *file, const pmFPAview *view)
+{
+    if (file->mode != PM_FPA_MODE_READ)
+        return false;
+
+    // get the current depth
+    pmFPAdepth depth = pmFPAviewDepth (view);
+
+    // do we need to open this file?
+    if (depth == file->fileDepth) {
+        pmFPAfileOpen (file, view);
+    }
+
+    // do we need to read this file?
+    if (depth != file->dataDepth)
+        return false;
+
+    switch (file->type) {
+    case PM_FPA_FILE_IMAGE:
+        if (pmFPAviewReadFitsImage (view, file)) {
+            psTrace ("pmFPAfile", 5, "reading %s (type: %d)\n", file->filename, file->type);
+        } else {
+            psTrace ("pmFPAfile", 5, "skipping %s (type: %d)\n", file->filename, file->type);
+        }
+        break;
+
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_CMF:
+        pmFPAviewReadObjects (view, file);
+        psTrace ("pmFPAfile", 5, "reading %s (type: %d)\n", file->filename, file->type);
+        break;
+
+    case PM_FPA_FILE_PSF:
+        pmFPAviewReadPSFmodel (view, file);
+        psTrace ("pmFPAfile", 5, "reading %s (type: %d)\n", file->filename, file->type);
+        break;
+
+    case PM_FPA_FILE_JPEG:
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAfileWrite (pmFPAfile *file, const pmFPAview *view)
+{
+    if (file->mode != PM_FPA_MODE_WRITE)
+        return false;
+
+    // get the current depth
+    pmFPAdepth depth = pmFPAviewDepth (view);
+
+    // do we need to write this file?
+    if (depth != file->dataDepth)
+        return false;
+
+    // do we need to open this file?
+    if (depth >= file->fileDepth) {
+        pmFPAfileOpen (file, view);
+    }
+
+    switch (file->type) {
+    case PM_FPA_FILE_IMAGE:
+        pmFPAviewWriteFitsImage (view, file);
+        psTrace ("pmFPAfile", 5, "wrote image %s (fpa: %p)\n", file->filename, file->fpa);
+        break;
+
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_CMF:
+        pmFPAviewWriteObjects (view, file);
+        psTrace ("pmFPAfile", 5, "wrote object %s (fpa: %p)\n", file->filename, file->fpa);
+        break;
+
+    case PM_FPA_FILE_PSF:
+        pmFPAviewWritePSFmodel (view, file);
+        psTrace ("pmFPAfile", 5, "wrote PSF %s (fpa: %p)\n", file->filename, file->fpa);
+        break;
+
+    case PM_FPA_FILE_JPEG:
+        pmFPAviewWriteJPEG (view, file);
+        psTrace ("pmFPAfile", 5, "wrote PSF %s (fpa: %p)\n", file->filename, file->fpa);
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        return false;
+    }
+    return true;
+}
+
+// create the data elements (headers, images) appropriate for this view
+bool pmFPAfileCreate (pmFPAfile *file, const pmFPAview *view)
+{
+    if (file->mode != PM_FPA_MODE_WRITE)
+        return false;
+
+    // get the current depth
+    pmFPAdepth depth = pmFPAviewDepth (view);
+
+    // do we need to write this file?
+    if (depth != file->dataDepth)
+        return false;
+
+    // XXX is this a sufficient check to avoid creating elements?
+    if (file->src == NULL)
+        return false;
+
+    switch (file->type) {
+    case PM_FPA_FILE_IMAGE:
+        /* create a PHU for thie file, if it does not exist */
+        pmFPAfileCopyStructureView (file->fpa, file->src, file->format, file->xBin, file->yBin, view);
+        psTrace ("pmFPAfile", 5, "created fpa data elements for %s (fpa: %p)\n", file->filename, file->fpa);
+        break;
+
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_CMF:
+    case PM_FPA_FILE_PSF:
+    case PM_FPA_FILE_JPEG:
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        return false;
+    }
+    return true;
+}
+
+bool pmFPAfileClose (pmFPAfile *file, const pmFPAview *view)
+{
+    if (file->state == PM_FPA_STATE_CLOSED) {
+        return true;
+    }
+
+    // is current depth == open depth?
+    pmFPAdepth depth = pmFPAviewDepth (view);
+    if (file->fileDepth != depth) {
+        return false;
+    }
+
+    // check if we are actually open
+    switch (file->type) {
+        // check the FITS types
+    case PM_FPA_FILE_IMAGE:
+    case PM_FPA_FILE_CMF:
+        psFitsClose (file->fits);
+        psTrace ("pmFPAfile", 5, "closing %s (type: %d)\n", file->filename, file->type);
+        file->fits = NULL;
+        file->phu = NULL;
+        file->header = NULL;
+        file->state = PM_FPA_STATE_CLOSED;
+        break;
+
+        // ignore the TEXT types
+    case PM_FPA_FILE_SX:
+    case PM_FPA_FILE_RAW:
+    case PM_FPA_FILE_OBJ:
+    case PM_FPA_FILE_CMP:
+    case PM_FPA_FILE_PSF:
+    case PM_FPA_FILE_JPEG:
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        return false;
+    }
+    return true;
+}
+
+// attempt open, read, write, or close pmFPAfiles in files
+bool pmFPAfileIOChecks (psMetadata *files, const pmFPAview *view, pmFPAfilePlace place)
+{
+    // recipe override values (command-line options):
+    psMetadataItem *item = NULL;
+    psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
+    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+        pmFPAfile *file = item->data.V;
+
+        if (place == PM_FPA_BEFORE) {
+            pmFPAfileRead (file, view);
+            pmFPAfileCreate (file, view);
+        } else {
+            pmFPAfileWrite (file, view);
+            pmFPAfileClose (file, view);
+        }
+    }
+    psFree (iter);
+    return true;
+}
+
+// create a file with the given name, assign it type "INTERNAL", and supply it with an image
+// of the requested dimensions. (image only, mask and weight are ignored)
+pmReadout *pmFPAfileCreateInternal (psMetadata *files, char *name, int Nx, int Ny, int type)
+{
+    pmReadout *readout = pmReadoutAlloc (NULL);
+    readout->image = psImageAlloc (Nx, Ny, type);
+
+    // I want an image from the
+    pmFPAfile *file = pmFPAfileAlloc();
+    file->mode = PM_FPA_MODE_INTERNAL;
+
+    file->readout = readout;
+    psMetadataAddPtr (files, PS_LIST_TAIL, name, PS_DATA_UNKNOWN, "", file);
+    psFree (file);
+    // we free this copy of file, but 'files' still has a copy
+
+    return (readout);
+}
+
+bool pmFPAfileDropInternal (psMetadata *files, char *name)
+{
+    bool status;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (file == NULL)
+        return false;
+
+    if (file->mode != PM_FPA_MODE_INTERNAL)
+        return false;
+
+    psMetadataRemoveKey (files, name);
+    return true;
+}
+
+// select the readout from the named pmFPAfile; if the named file does not exist,
+pmReadout *pmFPAfileThisReadout (psMetadata *files, const pmFPAview *view, const char *name)
+{
+    bool status;
+
+    pmFPAfile *file = psMetadataLookupPtr (&status, files, name);
+    if (file == NULL)
+        return NULL;
+
+    // internal files have the readout as a separate element:
+    if (file->mode == PM_FPA_MODE_INTERNAL) {
+        return file->readout;
+    }
+
+    pmReadout *readout = pmFPAviewThisReadout (view, file->fpa);
+    return readout;
+}
+
+// given an already-opened fits file, read the components corresponding
+// to the specified view
+bool pmFPAviewReadFitsImage (const pmFPAview *view, pmFPAfile *file)
+{
+    bool status;
+    pmFPA *fpa = file->fpa;
+    psFits *fits = file->fits;
+
+    if (view->chip == -1) {
+        status = pmFPARead (fpa, fits, NULL);
+        return status;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        status = pmChipRead (chip, fits, NULL);
+        return status;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        status = pmCellRead (cell, fits, NULL);
+        return status;
+    }
+    return false;
+
+    // XXX pmReadoutRead, pmReadoutReadSegement disabled for now
+    # if (0)
+
+        if (view->readout >= cell->readouts->n) {
+            return false;
+        }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (view->nRows == 0) {
+        pmReadoutRead (readout, fits, NULL);
+    } else {
+        pmReadoutReadSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
+    }
+    return true;
+    # endif
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view
+bool pmFPAviewWriteFitsImage (const pmFPAview *view, pmFPAfile *file)
+{
+
+    pmFPA *fpa = file->fpa;
+    psFits *fits = file->fits;
+
+    // pmFPAWrite takes care of all PHUs as needed
+    if (view->chip == -1) {
+        pmFPAWrite (fpa, fits, NULL);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    // do we need to write out a PHU for this entry?
+    if (file->phu == NULL) {
+        pmHDU *hdu = pmFPAviewThisHDU (view, file->fpa);
+        pmHDU *phu = pmFPAviewThisPHU (view, file->fpa);
+        // if this hdu is the phu, the write function below will create the phu
+        if (hdu != phu) {
+            // we assume that the PHU is just a header
+            psMetadata *outhead = psMetadataCopy (NULL, phu->header);
+            psMetadataAdd (outhead, PS_LIST_TAIL, "EXTEND", PS_DATA_BOOL | PS_META_REPLACE, "this file has extensions", true);
+            psFitsWriteHeaderNotImage (file->fits, outhead);
+            file->phu = phu->header;
+            psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+            psFree (outhead);
+        }
+    }
+
+    if (view->cell == -1) {
+        pmChipWrite (chip, fits, NULL);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWrite (cell, fits, NULL);
+        return true;
+    }
+    return false;
+
+    // XXX disable readout write for now
+    # if (0)
+
+        if (view->readout >= cell->readouts->n) {
+            return false;
+        }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    if (view->nRows == 0) {
+        pmReadoutWrite (readout, fits, NULL, NULL);
+    } else {
+        pmReadoutWriteSegment (readout, fits, view->nRows, view->iRows, NULL, NULL);
+    }
+    return true;
+    # endif
+}
+
+// look for the given name on the argument list.
+// returns the file (a view to the one saved on config->files)
+pmFPAfile *pmFPAfileFromArgs (bool *found, pmConfig *config, char *filename, char *argname)
+{
+    bool status;
+    pmFPA *fpa = NULL;
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+    psMetadata *format = NULL;
+
+    if (*found)
+        return NULL;
+
+    // we search the argument data for the named fileset (argname)
+    psArray *infiles = psMetadataLookupPtr(&status, config->arguments, argname);
+    if (!status)
+        return NULL;
+    if (infiles->n < 1)
+        return NULL;
+
+    // determine the current format from the header
+    // if no camera has been specified, use the first image as a template for the rest.
+    fits = psFitsOpen (infiles->data[0], "r");
+    phu = psFitsReadHeader (NULL, fits);
+    format = pmConfigCameraFormatFromHeader (config, phu);
+    psFitsClose (fits);
+
+    // build the template fpa, set up the basic view
+    fpa = pmFPAConstruct (config->camera);
+
+    // load the given filerule (from config->camera) and associate it with the fpa
+    // the output file is just a view to the file on config->files
+    file = pmFPAfileDefine (config->files, config->camera, fpa, filename);
+    if (!file) {
+        psErrorStackPrint(stderr, "file %s not defined\n", filename);
+        psFree (fpa);
+        psFree (format);
+        return NULL;
+    }
+
+    // this file is (by virtue of being supplied in the argument list) coming from the fileset
+    psFree (file->filerule);
+    psFree (file->filextra);
+
+    // adjust the rules to identify these files in the file->names data
+    file->filerule = psStringCopy ("@FILES");
+    // XXX this rule does not work in general
+    file->filextra = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    // examine the list of input files and validate their cameras
+    for (int i = 0; i < infiles->n; i++) {
+        if (i > 0) {
+            fits = psFitsOpen (infiles->data[i], "r");
+            phu = psFitsReadHeader (NULL, fits);
+            pmConfigValidateCameraFormat (format, phu);
+            psFitsClose (fits);
+        }
+
+        // set the view to the corresponding entry for this phu
+        pmFPAview *view = pmFPAAddSourceFromHeader (fpa, phu, format);
+
+        // XXX is this the correct psMD to save the filename?
+        char *name = pmFPAfileNameFromRule (file->filextra, file, view);
+        psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[i]);
+
+        psFree (view);
+        psFree (name);
+        psFree (phu);
+    }
+    psFree (fpa);
+    psFree (format);
+    *found = true;
+
+    return file;
+}
+
+// XXX this this function through, then finish
+# if 0
+// look for the given name on the argument list.
+// returns the file (a view to the one saved on config->files)
+// in this case, each file should correspond to the same view
+// (except at the readout level), of multiple FPAs
+// XXX think this through a bit more:
+//  - do we create multiple input fpas, or assign the same files to the single fpa?
+//  - do we save the multiple pmFPAfiles in the config->files psMD?
+//  - do we save the multiple filenames in the file->names psMD?
+//  - if we have a single FPA and multiple names, we need a method to
+//    turn on a specific name for the I/O actions.  probably true if we
+//    have multiple FPAs as well.
+pmFPAfile *pmFPAfileSetFromArgs (bool *found, pmConfig *config, char *filename, char *argname)
+{
+    bool status;
+    pmFPA *fpa = NULL;
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+    psMetadata *format = NULL;
+
+    if (*found)
+        return NULL;
+
+    // we search the argument data for the named fileset (argname)
+    psArray *infiles = psMetadataLookupPtr(&status, config->arguments, argname);
+    if (!status)
+        return NULL;
+    if (infiles->n < 1)
+        return NULL;
+
+    // determine the current format from the header
+    // if no camera has been specified, use the first image as a template for the rest.
+    fits = psFitsOpen (infiles->data[0], "r");
+    phu = psFitsReadHeader (NULL, fits);
+    format = pmConfigCameraFormatFromHeader (config, phu);
+    psFitsClose (fits);
+
+    // build the template fpa, set up the basic view
+    fpa = pmFPAConstruct (config->camera);
+
+    // load the given filerule (from config->camera) and associate it with the fpa
+    // the output file is just a view to the file on config->files
+    // XXX the filenames placed on config->files should include the seq number
+    file = pmFPAfileDefine (config->files, config->camera, fpa, filename);
+    if (!file) {
+        psErrorStackPrint(stderr, "file %s not defined\n", filename);
+        psFree (fpa);
+        psFree (format);
+        return NULL;
+    }
+
+    // this file is (by virtue of being supplied in the argument list) coming from the fileset
+    psFree (file->filerule);
+    psFree (file->filextra);
+
+    // adjust the rules to identify these files in the file->names data
+    file->filerule = psStringCopy ("@FILES");
+    // XXX this rule does not work in general
+    file->filextra = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    // examine the list of input files and validate their cameras
+    for (int i = 0; i < infiles->n; i++) {
+        if (i > 0) {
+            fits = psFitsOpen (infiles->data[i], "r");
+            phu = psFitsReadHeader (NULL, fits);
+            pmConfigValidateCameraFormat (format, phu);
+            psFitsClose (fits);
+        }
+
+        // set the view to the corresponding entry for this phu
+        pmFPAview *view = pmFPAAddSourceFromHeader (fpa, phu, format);
+
+        // XXX is this the correct psMD to save the filename?
+        char *name = pmFPAfileNameFromRule (file->filextra, file, view);
+        psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[i]);
+
+        psFree (view);
+        psFree (name);
+        psFree (phu);
+    }
+    psFree (fpa);
+    psFree (format);
+    *found = true;
+
+    return file;
+}
+# endif
+
+// create a new output pmFPAfile based on an existing FPA
+pmFPAfile *pmFPAfileFromFPA (pmConfig *config, pmFPA *src, int xBin, int yBin, char *filename)
+{
+    // XXX pmFPAConstruct has many leaks (6919)
+    pmFPA *fpa = pmFPAConstruct (config->camera);
+    pmFPAfile *file = pmFPAfileDefine (config->files, config->camera, fpa, filename);
+    file->src = src; // inherit output elements from this source pmFPA
+    file->xBin = xBin;
+    file->yBin = yBin;
+
+    return file;
+}
+
+// look for the given name on the argument list.
+pmFPAfile *pmFPAfileFromConf (bool *found, pmConfig *config, char *filename, pmFPA *input)
+{
+    psFits *fits = NULL;
+    pmFPAfile *file = NULL;
+    psMetadata *phu = NULL;
+    psMetadata *format = NULL;
+    psArray *infiles = NULL;
+
+    if (*found)
+        return NULL;
+
+    // a camera config is needed (as source of file rule)
+    if (config->camera == NULL) {
+        psErrorStackPrint (stderr, "camera is not defined\n");
+        return NULL;
+    }
+
+    // expect @DETDB in the config filerule
+    file = pmFPAfileDefine (config->files, config->camera, NULL, filename);
+    if (!file) {
+        psErrorStackPrint(stderr, "file %s not defined\n", filename);
+        return NULL;
+    }
+
+    // image names come from the file->name list?
+    if (!strcasecmp (file->filerule, "@FILES"))
+        psAbort ("pmFPAfileFromConfig", "programming error");
+
+    // image needs to come from the detrend database
+    if (!strcasecmp (file->filerule, "@DETDB")) {
+        // char *extra = pmFPAfileNameFromRule (file->filextra, file, view);
+        // psArray *infiles = pmDetrendSelect (extra, input);
+        psAbort ("pmFPAfileFromConfig", "@DETDB not yet defined");
+    } else {
+        infiles = psArrayAlloc(1);
+        infiles->n = 1;
+        infiles->data[0] = psStringCopy (file->filerule);
+    }
+    if (infiles == NULL)
+        return NULL;
+    if (infiles->n < 1) {
+        psFree (infiles);
+        return NULL;
+    }
+
+    // determine the current format from the header
+    // if no camera has been specified, use the first image as a template for the rest.
+    fits = psFitsOpen (infiles->data[0], "r");
+    phu = psFitsReadHeader (NULL, fits);
+    format = pmConfigCameraFormatFromHeader (config, phu);
+    psFitsClose (fits);
+
+    // build the template fpa, set up the basic view
+    file->fpa = pmFPAConstruct (config->camera);
+
+    // this file is (by virtue of being supplied in the argument list) coming from the fileset
+    psFree (file->filerule);
+    psFree (file->filextra);
+
+    // adjust the rules to identify these files in the file->names data
+    file->filerule = psStringCopy ("@FILES");
+    // XXX this rule does not work in general
+    file->filextra = psStringCopy ("{CHIP.NAME}.{CELL.NAME}");
+
+    // examine the list of input files and validate their cameras
+    for (int i = 0; i < infiles->n; i++) {
+        if (i > 0) {
+            fits = psFitsOpen (infiles->data[i], "r");
+            phu = psFitsReadHeader (NULL, fits);
+            pmConfigValidateCameraFormat (format, phu);
+            psFitsClose (fits);
+        }
+
+        // set the view to the corresponding entry for this phu
+        pmFPAview *view = pmFPAAddSourceFromHeader (file->fpa, phu, format);
+
+        // XXX is this the correct psMD to save the filename?
+        char *name = pmFPAfileNameFromRule (file->filextra, file, view);
+        psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", infiles->data[i]);
+
+        psFree (view);
+        psFree (name);
+        psFree (phu);
+    }
+    psFree (format);
+    psFree (infiles);
+    *found = true;
+    return file;
+}
+
+bool pmFPAfileAddFileNames (psMetadata *files, char *name, char *value, int mode)
+{
+
+    // add the output names to the output-type files
+    psMetadataItem *item = NULL;
+    psMetadataIterator *iter = psMetadataIteratorAlloc (files, PS_LIST_HEAD, NULL);
+    while ((item = psMetadataGetAndIncrement (iter)) != NULL) {
+        pmFPAfile *file = item->data.V;
+
+        if (file->mode == mode) {
+            psMetadataAddStr (file->names, PS_LIST_TAIL, name, 0, "", value);
+        }
+    }
+    psFree (iter);
+    return true;
+}
+
+// select the rule from the camera configuration, perform substitutions as needed
+char *pmFPAfileNameFromRule (char *rule, pmFPAfile *file, const pmFPAview *view)
+{
+
+    char *newName = NULL;     // destination for resulting name
+
+    newName = psStringCopy (rule);
+
+    if (strstr (newName, "{CHIP.NAME}") != NULL) {
+        pmChip *chip = pmFPAviewThisChip (view, file->fpa);
+        if (chip != NULL) {
+            char *name = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+            if (name != NULL) {
+                newName = psStringSubstitute (newName, name, "{CHIP.NAME}");
+            }
+        }
+    }
+    if (strstr (newName, "{CELL.NAME}") != NULL) {
+        pmCell *cell = pmFPAviewThisCell (view, file->fpa);
+        if (cell != NULL) {
+            char *name = psMetadataLookupStr (NULL, cell->concepts, "CELL.NAME");
+            if (name != NULL) {
+                newName = psStringSubstitute (newName, name, "{CELL.NAME}");
+            }
+        }
+    }
+    if (strstr (newName, "{EXTNAME}") != NULL) {
+        pmHDU *hdu = pmFPAviewThisHDU (view, file->fpa);
+        if (hdu->extname != NULL) {
+            newName = psStringSubstitute (newName, hdu->extname, "{EXTNAME}");
+        }
+    }
+    if (strstr (newName, "{OUTPUT}") != NULL) {
+        char *name = psMetadataLookupStr (NULL, file->names, "OUTPUT");
+        if (name != NULL) {
+            newName = psStringSubstitute (newName, name, "{OUTPUT}");
+        }
+    }
+    return newName;
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view
+bool pmFPAfileCopyView (pmFPA *out, pmFPA *in, const pmFPAview *view)
+{
+    // pmFPAWrite takes care of all PHUs as needed
+    if (view->chip == -1) {
+        pmFPACopy (out, in);
+        return true;
+    }
+    if (view->chip >= in->chips->n) {
+        return false;
+    }
+    pmChip *inChip = in->chips->data[view->chip];
+    pmChip *outChip = out->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipCopy (outChip, inChip);
+        return true;
+    }
+    if (view->cell >= inChip->cells->n) {
+        return false;
+    }
+    pmCell *inCell = inChip->cells->data[view->cell];
+    pmCell *outCell = outChip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellCopy (outCell, inCell);
+        return true;
+    }
+    return false;
+
+    // XXX add readout / segment equivalents
+}
+
+// given an already-opened fits file, write the components corresponding
+// to the specified view
+bool pmFPAfileCopyStructureView (pmFPA *out, pmFPA *in, psMetadata *format, int xBin, int yBin, const pmFPAview *view)
+{
+    // pmFPAWrite takes care of all PHUs as needed
+    if (view->chip == -1) {
+        pmFPAAddSourceFromView (out, view, format);
+        pmFPACopyStructure (out, in, xBin, yBin);
+        return true;
+    }
+    if (view->chip >= in->chips->n) {
+        return false;
+    }
+    pmChip *inChip = in->chips->data[view->chip];
+    pmChip *outChip = out->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmFPAAddSourceFromView (out, view, format);
+        pmChipCopyStructure (outChip, inChip, xBin, yBin);
+        return true;
+    }
+    if (view->cell >= inChip->cells->n) {
+        return false;
+    }
+    pmCell *inCell = inChip->cells->data[view->cell];
+    pmCell *outCell = outChip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmFPAAddSourceFromView (out, view, format);
+        pmCellCopyStructure (outCell, inCell, xBin, yBin);
+        return true;
+    }
+    return false;
+
+    // XXX add readout / segment equivalents
+}
Index: trunk/psModules/src/astrom/pmFPAfile.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAfile.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAfile.h	(revision 6872)
@@ -0,0 +1,155 @@
+/** @file  pmFPAview.h
+*
+*  @brief Tools to manipulate the FPA structure elements.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
+*
+*  Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+*/
+
+#ifndef PM_FPA_FILE_H
+#define PM_FPA_FILE_H
+
+#include "pslib.h"
+#include "pmConfig.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+
+typedef enum {
+    PM_FPA_BEFORE,
+    PM_FPA_AFTER,
+} pmFPAfilePlace;
+
+typedef enum {
+    PM_FPA_FILE_NONE,
+    PM_FPA_FILE_SX,
+    PM_FPA_FILE_OBJ,
+    PM_FPA_FILE_CMP,
+    PM_FPA_FILE_CMF,
+    PM_FPA_FILE_RAW,
+    PM_FPA_FILE_IMAGE,
+    PM_FPA_FILE_PSF,
+    PM_FPA_FILE_JPEG,
+} pmFPAfileType;
+
+typedef enum {
+    PM_FPA_MODE_NONE,
+    PM_FPA_MODE_READ,
+    PM_FPA_MODE_WRITE,
+    PM_FPA_MODE_INTERNAL,
+} pmFPAfileMode;
+
+typedef enum {
+    PM_FPA_STATE_OPEN,
+    PM_FPA_STATE_CLOSED,
+} pmFPAfileState;
+
+typedef struct
+{
+    pmFPAfileMode mode;   // is this file read, written, or only used internally?
+    pmFPAfileType type;   // what type of data is read from / written to disk?
+    pmFPAfileState state;  // have we opened the file, etc?
+
+    pmFPAdepth fileDepth;  // what depth in the FPA hierarchy represents a unique file?
+    pmFPAdepth dataDepth;  // at what depth do we read/write the data segment?
+
+    pmFPA *fpa;    // for I/O files, we carry a pointer to the complete fpa
+    psFits *fits;   // for I/O files of fits type (IMAGE, CMP, CMF), we carry a file handle
+
+    psMetadata *phu;   // pointer (view) to the current phu header
+    psMetadata *header;   // pointer (view) to the current hdu header
+
+    pmReadout *readout;   // for internal files, we only carry a single readout
+
+    psMetadata *names;   // filenames supplied by the cmdline or detdb are saved here
+
+    char *filerule;   // rule for constructing a filename when needed
+    char *filextra;   // additional information used to define filenames (context dependent)
+    char *extrule;   // rule for constructing an extension name when needed
+    char *extxtra;   // additional information used to define extension names (context dependent)
+
+    char *name;    // the name of the rule (useful for debugging / tracing)
+    char *filename;   // the current name of an active file
+    char *extname;   // the current name of an active file extension
+
+    // the following elements are used for WRITE-mode IMAGE-type pmFPAfiles to inform
+    // the creation of a new image based on an existing image
+    pmFPA *src;    // if an output FPA, inherit from this FPA
+    int xBin;    // desired binning in x direction
+    int yBin;    // desired binning in y direction
+    psMetadata *format;
+}
+pmFPAfile;
+
+// allocate an empty pmFPAfile structure
+pmFPAfile *pmFPAfileAlloc ();
+
+// load the pmFPAfile information from the camera configuration data
+pmFPAfile *pmFPAfileDefine (psMetadata *files, psMetadata *camera, pmFPA *fpa, char *name);
+
+// load the pmFPAfile information from the camera configuration data, constructing the needed fpa structure
+// XXX deprecate this function?
+// pmFPAfile *pmFPAfileConstruct (psMetadata *files, psMetadata *format, psMetadata *camera, char *name);
+
+// open the real file corresponding to the given pmFPAfile appropriate to the current view
+bool pmFPAfileOpen (pmFPAfile *file, const pmFPAview *view);
+
+// read from the real file corresponding to the given pmFPAfile for the current view
+bool pmFPAfileRead (pmFPAfile *file, const pmFPAview *view);
+
+bool pmFPAfileCreate (pmFPAfile *file, const pmFPAview *view);
+
+// write to the real file corresponding to the given pmFPAfile for the current view
+bool pmFPAfileWrite (pmFPAfile *file, const pmFPAview *view);
+
+// close the real file corresponding to the given pmFPAfile appropriate to the current view
+bool pmFPAfileClose (pmFPAfile *file, const pmFPAview *view);
+
+// examine all pmFPAfiles listed in the files and perform the needed I/O operations (open,read,write,close)
+bool pmFPAfileIOChecks (psMetadata *files, const pmFPAview *view, pmFPAfilePlace place);
+
+// return an image corresponding to the current readout, from the specified file.  if the pmFPAfile does not
+// exist, construct the image using the given size and type (save it in a pmFPAfile??)
+// psImage *pmFPAfileReadoutImage (psMetadata *files, const pmFPAview *view, char *name, int Nx, int Ny, int type);
+
+// select the readout from the named pmFPAfile; if the named file does not exist,
+pmReadout *pmFPAfileThisReadout (psMetadata *files, const pmFPAview *view, const char *name);
+
+// create a file with the given name, assign it type "INTERNAL", and supply it with an image
+// of the requested dimensions. (image only, mask and weight are ignored)
+pmReadout *pmFPAfileCreateInternal (psMetadata *files, char *name, int Nx, int Ny, int type);
+
+// delete the INTERNAL file of the given name (if it exists)
+bool pmFPAfileDropInternal (psMetadata *files, char *name);
+
+// read an image into the current view
+bool pmFPAviewReadFitsImage (const pmFPAview *view, pmFPAfile *file);
+
+// write the components for the specified view
+bool pmFPAviewWriteFitsImage (const pmFPAview *view, pmFPAfile *file);
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+pmFPAfile *pmFPAfileFromArgs (bool *found, pmConfig *config, char *filename, char *argname);
+
+// look for the given argname on the argument list.  find the give filename from the file rules
+pmFPAfile *pmFPAfileFromConf (bool *found, pmConfig *config, char *filename, pmFPA *input);
+
+// create a new output pmFPAfile based on an existing FPA
+pmFPAfile *pmFPAfileFromFPA (pmConfig *config, pmFPA *src, int xBin, int yBin, char *filename);
+
+// add the specified filename info (value) to the files of the given mode using the given reference name
+bool pmFPAfileAddFileNames (psMetadata *files, char *name, char *value, int mode);
+
+// convert the rule to a name based on the current view
+char *pmFPAfileNameFromRule (char *rule, pmFPAfile *file, const pmFPAview *view);
+
+bool pmFPAfileCopyView (pmFPA *out, pmFPA *in, const pmFPAview *view);
+
+bool pmFPAfileCopyStructureView (pmFPA *out, pmFPA *in, psMetadata *format, int xBin, int yBin, const pmFPAview *view);
+
+# endif
Index: trunk/psModules/src/astrom/pmFPAview.c
===================================================================
--- trunk/psModules/src/astrom/pmFPAview.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAview.c	(revision 6872)
@@ -0,0 +1,267 @@
+/** @file  pmFPAview.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:04 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+
+static void pmFPAviewFree (pmFPAview *view)
+{
+    if (view == NULL)
+        return;
+    return;
+}
+
+pmFPAview *pmFPAviewAlloc (int nRows)
+{
+    pmFPAview *view = psAlloc (sizeof(pmFPAview));
+    psMemSetDeallocator (view, (psFreeFunc) pmFPAviewFree);
+
+    view->chip    = -1;
+    view->cell    = -1;
+    view->readout = -1;
+    view->iRows   =  0;
+    view->nRows   = nRows;
+    return (view);
+}
+
+pmFPAdepth pmFPAviewDepth (const pmFPAview *view)
+{
+
+    if (view->chip < 0) {
+        return PM_FPA_DEPTH_FPA;
+    }
+    if (view->cell < 0) {
+        return PM_FPA_DEPTH_CHIP;
+    }
+    if (view->readout < 0) {
+        return PM_FPA_DEPTH_CELL;
+    }
+    return PM_FPA_DEPTH_READOUT;
+}
+
+pmChip *pmFPAviewThisChip (const pmFPAview *view, pmFPA *fpa)
+{
+
+    if (view->chip < 0) {
+        return NULL;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return NULL;
+    }
+
+    pmChip *chip = fpa->chips->data[view->chip];
+    return chip;
+}
+
+pmChip *pmFPAviewNextChip (pmFPAview *view, pmFPA *fpa, int nStep)
+{
+    view->cell = -1;
+    view->readout = -1;
+    view->iRows = 0;
+
+    // if there are no available chips, return NULL
+    if (fpa->chips->n <= 0) {
+        view->chip = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->chip < -1) {
+        view->chip = -1;
+    }
+
+    // increment to the next chip
+    view->chip += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->chip >= fpa->chips->n) {
+        view->chip = -1;
+        return NULL;
+    }
+
+    // get the correct chip pointer
+    pmChip *chip = fpa->chips->data[view->chip];
+    return (chip);
+}
+
+pmCell *pmFPAviewThisCell (const pmFPAview *view, pmFPA *fpa)
+{
+
+    if (view->cell < 0) {
+        return NULL;
+    }
+
+    pmChip *chip = pmFPAviewThisChip (view, fpa);
+    if (chip == NULL) {
+        return NULL;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return NULL;
+    }
+
+    pmCell *cell = chip->cells->data[view->cell];
+    return cell;
+}
+
+pmCell *pmFPAviewNextCell (pmFPAview *view, pmFPA *fpa, int nStep)
+{
+
+    pmChip *chip = pmFPAviewThisChip (view, fpa);
+    if (chip == NULL) {
+        return NULL;
+    }
+
+    view->readout = -1;
+    view->iRows = 0;
+
+    // if there are no available cells, return NULL
+    if (chip->cells->n <= 0) {
+        view->cell = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->cell < -1) {
+        view->cell = -1;
+    }
+
+    // increment to the next cell
+    view->cell += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->cell >= chip->cells->n) {
+        view->cell = -1;
+        return NULL;
+    }
+
+    // get the correct cell pointer
+    pmCell *cell = chip->cells->data[view->cell];
+    return (cell);
+}
+
+pmReadout *pmFPAviewThisReadout (const pmFPAview *view, pmFPA *fpa)
+{
+
+    if (view->readout < 0) {
+        return NULL;
+    }
+
+    pmCell *cell = pmFPAviewThisCell (view, fpa);
+    if (cell == NULL) {
+        return NULL;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return NULL;
+    }
+
+    pmReadout *readout = cell->readouts->data[view->readout];
+    return readout;
+}
+
+pmReadout *pmFPAviewNextReadout (pmFPAview *view, pmFPA *fpa, int nStep)
+{
+
+    pmCell *cell = pmFPAviewThisCell (view, fpa);
+    if (cell == NULL) {
+        return NULL;
+    }
+
+    view->iRows = 0;
+
+    // if there are no available cells, return NULL
+    if (cell->readouts->n <= 0) {
+        view->readout = -1;
+        return NULL;
+    }
+
+    // clean up < -1 values
+    if (view->readout < -1) {
+        view->readout = -1;
+    }
+
+    // increment to the next cell
+    view->readout += nStep;
+
+    // if we are at the end of the stack, return NULL
+    if (view->readout >= cell->readouts->n) {
+        view->readout = -1;
+        return NULL;
+    }
+
+    // get the correct cell pointer
+    pmReadout *readout = cell->readouts->data[view->readout];
+    return (readout);
+}
+
+pmHDU *pmFPAviewThisHDU (const pmFPAview *view, pmFPA *fpa)
+{
+    // the HDU is attached to a cell, chip or fpa
+    // if this view has a -1 for the level which contains the hdu,
+    // there is no unambiguous HDU
+
+    if (view->chip < 0) {
+        return pmHDUFromFPA (fpa);
+    }
+    if (view->cell < 0) {
+        return pmHDUFromChip (pmFPAviewThisChip (view, fpa));
+    }
+    if (view->readout < 0) {
+        return pmHDUFromCell (pmFPAviewThisCell (view, fpa));
+    }
+    return NULL;
+}
+
+pmHDU *pmFPAviewThisPHU (const pmFPAview *view, pmFPA *fpa)
+{
+    // select the HDU which corresponds to the PHU containing this view
+
+    pmHDU *hdu;
+    pmFPAview new;
+    pmChip *chip;
+    pmCell *cell;
+
+    new = *view;
+
+    if (view->chip < 0) {
+        hdu = pmHDUFromFPA (fpa);
+        if (hdu->phu)
+            return hdu;
+        return NULL;
+    }
+    if (view->cell < 0) {
+        chip = pmFPAviewThisChip (view, fpa);
+        hdu  = pmHDUFromChip (chip);
+        if (hdu->phu)
+            return hdu;
+        new.chip = -1;
+        hdu = pmFPAviewThisPHU (&new, fpa);
+        return hdu;
+    }
+    if (view->readout < 0) {
+        cell = pmFPAviewThisCell (view, fpa);
+        hdu  = pmHDUFromCell (cell);
+        if (hdu->phu)
+            return hdu;
+        new.cell = -1;
+        hdu = pmFPAviewThisPHU (&new, fpa);
+        return hdu;
+    }
+    return NULL;
+}
Index: trunk/psModules/src/astrom/pmFPAview.h
===================================================================
--- trunk/psModules/src/astrom/pmFPAview.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmFPAview.h	(revision 6872)
@@ -0,0 +1,70 @@
+/** @file  pmFPAview.h
+*
+*  @brief Tools to manipulate the FPA structure elements.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2006-04-17 18:01:04 $
+*
+*  Copyright 2004-2005 Institute for Astronomy, University of Hawaii
+*/
+
+#ifndef PM_FPA_VIEW_H
+#define PM_FPA_VIEW_H
+
+#include "pmFPA.h"
+
+/// @addtogroup AstroImage
+/// @{
+
+// depth of interest
+typedef enum {
+    PM_FPA_DEPTH_NONE,
+    PM_FPA_DEPTH_FPA,
+    PM_FPA_DEPTH_CHIP,
+    PM_FPA_DEPTH_CELL,
+    PM_FPA_DEPTH_READOUT,
+} pmFPAdepth;
+
+typedef struct
+{
+    int chip;                           // Number of the chip, or -1 for all
+    int cell;                           // Number of the cell, or -1 for all
+    int readout;                        // Number of the readout, or -1 for all
+    int nRows;                          // Maximum number of rows per readout segment read, or 0 for all
+    int iRows;                          // Starting point for this read
+}
+pmFPAview;
+
+// allocate a pmFPAview structure for this fpa and camera
+pmFPAview *pmFPAviewAlloc (int nRows);
+
+// determine the current view depth
+pmFPAdepth pmFPAviewDepth (const pmFPAview *view);
+
+// return the currently selected chip for this view
+pmChip *pmFPAviewThisChip (const pmFPAview *view, pmFPA *fpa);
+
+// advance view to the next chip
+pmChip *pmFPAviewNextChip (pmFPAview *view, pmFPA *fpa, int nStep);
+
+// return the currently selected cell for this view
+pmCell *pmFPAviewThisCell (const pmFPAview *view, pmFPA *fpa);
+
+// advance view to the next cell
+pmCell *pmFPAviewNextCell (pmFPAview *view, pmFPA *fpa, int nStep);
+
+// return the currently selected readout for this view
+pmReadout *pmFPAviewThisReadout (const pmFPAview *view, pmFPA *fpa);
+
+// advance view to the next readout
+pmReadout *pmFPAviewNextReadout (pmFPAview *view, pmFPA *fpa, int nStep);
+
+// return the HDU corresponding to the current view
+pmHDU *pmFPAviewThisHDU (const pmFPAview *view, pmFPA *fpa);
+pmHDU *pmFPAviewThisPHU (const pmFPAview *view, pmFPA *fpa);
+
+# endif
Index: trunk/psModules/src/astrom/pmHDU.c
===================================================================
--- trunk/psModules/src/astrom/pmHDU.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmHDU.c	(revision 6872)
@@ -0,0 +1,225 @@
+#include <stdio.h>
+#include <assert.h>
+
+#include "pslib.h"
+#include "pmFPA.h"
+#include "psAdditionals.h"
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// File-static (private) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Move to the appropriate extension in FITS file for HDU
+static bool hduMove(pmHDU *hdu,         // HDU with extname
+                    psFits *fits        // FITS file in which to move
+                   )
+{
+    // Deal with the PHU case
+    if (strcasecmp(hdu->extname, "PHU") == 0 || hdu->phu) {
+        if (! psFitsMoveExtNum(fits, 0, false)) {
+            psError(PS_ERR_IO, false, "Unable to move to primary header!\n");
+            return false;
+        }
+        hdu->phu = true;
+        return true;
+    }
+
+    if (! psFitsMoveExtName(fits, hdu->extname)) {
+        psError(PS_ERR_IO, false, "Unable to move to extension %s\n", hdu->extname);
+        return false;
+    }
+    // Now, just in case for some reason the PHU has an extension name that we've moved to....
+    if (psFitsGetExtNum(fits) == 0) {
+        hdu->phu = true;
+    } else {
+        hdu->phu = false;
+    }
+
+    return true;
+}
+
+static void hduFree(pmHDU *hdu)
+{
+    psFree(hdu->extname);
+    psFree(hdu->format);
+    psFree(hdu->header);
+    psFree(hdu->images);
+    psFree(hdu->table);
+    psFree(hdu->weights);
+    psFree(hdu->masks);
+}
+
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+pmHDU *pmHDUAlloc(const char *extname   // Extension name
+                 )
+{
+    pmHDU *hdu = psAlloc(sizeof(pmHDU));
+    psMemSetDeallocator(hdu, (psFreeFunc)hduFree);
+
+    if (! extname || strlen(extname) == 0) {
+        hdu->phu = true;
+        hdu->extname = psStringCopy("PHU");
+    } else {
+        if (strcasecmp(extname, "PHU") == 0) {
+            hdu->phu = true;
+        } else {
+            hdu->phu = false;
+        }
+        hdu->extname = psStringCopy(extname);
+    }
+    hdu->format  = NULL;
+    hdu->header  = NULL;
+    hdu->images  = NULL;
+    hdu->table   = NULL;
+    hdu->weights = NULL;
+    hdu->masks   = NULL;
+
+    return hdu;
+}
+
+bool pmHDUReadHeader(pmHDU *hdu,        // HDU for which to read header
+                     psFits *fits       // FITS file from which to read
+                    )
+{
+    assert(hdu);
+    assert(fits);
+
+    // Move to the appropriate extension
+    psTrace(__func__, 5, "Moving to extension %s...\n", hdu->extname);
+    if (!hduMove(hdu, fits)) {
+        return false;
+    }
+
+    if (!hdu->header) {
+        psTrace(__func__, 5, "Reading the header...\n");
+        hdu->header = psFitsReadHeader(NULL, fits);
+        if (! hdu->header) {
+            psError(PS_ERR_IO, false, "Unable to read header for extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+// Read the HDU
+// XXX: Add a region specifier?
+bool pmHDURead(pmHDU *hdu,              // HDU to read
+               psFits *fits             // FITS file to read from
+              )
+{
+    assert(hdu);
+    assert(fits);
+
+    // Read the header; includes the move
+    if (!pmHDUReadHeader(hdu, fits)) {
+        return false;
+    }
+
+    #ifdef FITS_TABLES
+    // What type is it?
+    if (psFitsIsImage(hdu->header)) {
+        #endif
+        if (hdu->images) {
+            psLogMsg(__func__, PS_LOG_WARN, "HDU %s has already been read --- overwriting.\n", hdu->extname);
+            psFree(hdu->images);        // Blow away anything existing
+        }
+        psTrace(__func__, 5, "Reading the pixels...\n");
+        hdu->images = psFitsReadImageCube(fits, psRegionSet(0,0,0,0));
+        if (! hdu->images) {
+            psError(PS_ERR_IO, false, "Unable to read pixels for extension %s\n", hdu->extname);
+            return false;
+        }
+        return true;
+        #ifdef FITS_TABLES
+
+    }
+    if (psFitsIsTable(hdu->table)) {
+        // Read the table
+        if (hdu->table) {
+            psFree(hdu->table);         // Blow away anything existing
+        }
+        hdu->table = psFitsReadTable(fits);
+        return true;
+    }
+
+    psError(PS_ERR_UNKNOWN, true, "No idea what this HDU consists of!\n");
+    return false;
+    #endif
+}
+
+// Write the HDU
+// XXX: Add a region specifier?
+bool pmHDUWrite(pmHDU *hdu,             // HDU to write
+                psFits *fits            // FITS file to write to
+               )
+{
+    assert(hdu);
+    assert(fits);
+
+    psTrace(__func__, 7, "Writing HDU %s\n", hdu->extname);
+
+    if (hdu->images && hdu->table && !hdu->header) {
+        psError(PS_ERR_IO, true, "Both image and table data provided in HDU, but no header --- "
+                "don't know which to write!\n");
+        return false;
+    }
+
+    if (!hdu->images && !hdu->table && !hdu->header) {
+        psLogMsg(__func__, PS_LOG_WARN, "Nothing to write for HDU %s\n", hdu->extname);
+        return false;
+    }
+
+    // Preserve the extension name, if it's the PHU
+    char *extname = hdu->extname;       // The name of the extension
+    if (strcasecmp(extname, "PHU") == 0 && hdu->header) {
+        bool mdok = true;               // Status of MD lookup
+        extname = psMetadataLookupStr(&mdok, hdu->header, "EXTNAME");
+        if (!mdok || !extname || strlen(extname) == 0) {
+            extname = "";
+        }
+    }
+
+    // Only a header
+    if (!hdu->images && !hdu->table) {
+        // Tell CFITSIO there's nothing there
+        psMetadataItem *naxis = psMetadataLookup(hdu->header, "NAXIS");
+        if (naxis) {
+            naxis->data.S32 = 0;
+        }
+
+        if (!psFitsWriteHeader(hdu->header, fits)) {
+            psError(PS_ERR_IO, false, "Unable to write header for extension %s\n", extname);
+        }
+    }
+
+    #ifdef FITS_TABLES
+    if (hdu->images && (!hdu->table || psFitsIsImage(hdu->header))) {
+        psFitsWriteImageCube(fits, hdu->header, hdu->images, extname);
+        return true;
+    }
+
+    if (hdu->table && (!hdu->images || psFitsIsTable(hdu->header))) {
+        bool psFitsWriteTable(psFits* fits, const psMetadata *header, const psArray* table);
+        return true;
+    }
+
+    psError(PS_ERR_IO, true, "No idea what this HDU consists of!\n");
+    return false;
+    #else
+
+    if (hdu->images) {
+        psTrace(__func__, 9, "Writing pixels for %s\n", hdu->extname);
+        if (!psFitsWriteImageCube(fits, hdu->header, hdu->images, extname)) {
+            psError(PS_ERR_IO, false, "Unable to write image to extension %s\n", hdu->extname);
+            return false;
+        }
+    }
+    return true;
+    #endif
+}
+
Index: trunk/psModules/src/astrom/pmHDU.h
===================================================================
--- trunk/psModules/src/astrom/pmHDU.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmHDU.h	(revision 6872)
@@ -0,0 +1,37 @@
+#ifndef PM_HDU_H
+#define PM_HDU_H
+
+// An instance of the FITS Header Data Unit
+typedef struct
+{
+    psString extname;                   // The extension name
+    bool phu;                           // Is this the FITS Primary Header Unit
+    psMetadata *format;                 // The camera format
+    psMetadata *header;                 // The FITS header, or NULL if primary for FITS; or section info
+    psArray *images;                    // The pixel data
+    psArray *weights;                   // The pixel data
+    psArray *masks;                     // The pixel data
+    psArray *table;                     // The table data
+}
+pmHDU;
+
+
+// Allocators
+pmHDU *pmHDUAlloc(const char *extname);
+
+// Read the header
+bool pmHDUReadHeader(pmHDU *hdu,        // HDU for which to read header
+                     psFits *fits       // FITS file from which to read
+                    );
+
+// Read the HDU
+bool pmHDURead(pmHDU *hdu,              // HDU to read
+               psFits *fits             // FITS file to read from
+              );
+
+// Write the HDU
+bool pmHDUWrite(pmHDU *hdu,             // HDU to write
+                psFits *fits            // FITS file to write to
+               );
+
+#endif
Index: trunk/psModules/src/astrom/pmHDUUtils.c
===================================================================
--- trunk/psModules/src/astrom/pmHDUUtils.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmHDUUtils.c	(revision 6872)
@@ -0,0 +1,60 @@
+#include <stdio.h>
+#include "pmFPA.h"
+#include "pmHDU.h"
+#include "pmHDUUtils.h"
+
+
+pmHDU *pmHDUFromFPA(pmFPA *fpa          // FPA for which to find HDU
+                   )
+{
+    return fpa->hdu;
+}
+
+pmHDU *pmHDUFromChip(pmChip *chip       // Chip for which to find HDU
+                    )
+{
+    pmHDU *hdu = chip->hdu;             // The HDU information
+    if (!hdu) {
+        hdu = pmHDUFromFPA(chip->parent); // Grab HDU info from the FPA
+    }
+
+    return hdu;
+}
+
+pmHDU *pmHDUFromCell(pmCell *cell       // Cell for which to find HDU
+                    )
+{
+    pmHDU *hdu = cell->hdu;             // The HDU information
+    if (!hdu) {
+        hdu = pmHDUFromChip(cell->parent); // Grab HDU info from the chip
+    }
+
+    return hdu;
+}
+
+pmHDU *pmHDUFromReadout (pmReadout *readout)
+{
+
+    pmCell *cell = readout->parent; // cell containing this readout;
+    pmHDU *hdu = pmHDUFromCell (cell);
+    return hdu;
+}
+
+// Get the lowest HDU
+pmHDU *pmHDUGetLowest(pmFPA *fpa, // The FPA
+                      pmChip *chip, // The chip, or NULL
+                      pmCell *cell // The cell, or NULL
+                     )
+{
+    pmHDU *hdu = NULL;          // The HDU that's at the lowest level
+    if (cell) {
+        hdu = pmHDUFromCell(cell);
+    } else if (chip) {
+        hdu = pmHDUFromChip(chip);
+    } else if (fpa) {
+        hdu = pmHDUFromFPA(fpa);
+    }
+
+    return hdu;
+}
+
Index: trunk/psModules/src/astrom/pmHDUUtils.h
===================================================================
--- trunk/psModules/src/astrom/pmHDUUtils.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmHDUUtils.h	(revision 6872)
@@ -0,0 +1,24 @@
+#ifndef PM_HDU_UTILS_H
+#define PM_HDU_UTILS_H
+
+#include "pmFPA.h"
+#include "pmHDU.h"
+
+// Get the lowest HDU in the hierarchy, as supplied
+pmHDU *pmHDUGetLowest(pmFPA *fpa, // The FPA
+                      pmChip *chip, // The chip, or NULL
+                      pmCell *cell // The cell, or NULL
+                     );
+
+// Find the HDU in the FPA hierarchy
+pmHDU *pmHDUFromFPA(pmFPA *fpa          // FPA for which to find HDU
+                   );
+pmHDU *pmHDUFromChip(pmChip *chip       // Chip for which to find HDU
+                    );
+pmHDU *pmHDUFromCell(pmCell *cell       // Cell for which to find HDU
+                    );
+
+pmHDU *pmHDUFromReadout (pmReadout *readout  // Readout for which to find HDU
+                        );
+
+#endif
Index: trunk/psModules/src/astrom/pmReadout.c
===================================================================
--- trunk/psModules/src/astrom/pmReadout.c	(revision 6872)
+++ trunk/psModules/src/astrom/pmReadout.c	(revision 6872)
@@ -0,0 +1,109 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmHDUUtils.h"
+#include "pmFPAMaskWeight.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))) {
+        psImage *bias = psMemIncrRefCounter(psImageSubset(image, *region)); // Image from bias section
+        psListAdd(images, PS_LIST_TAIL, bias);
+        psFree(bias);
+    }
+    psFree(sectionsIter);
+
+    return images;
+}
+
+bool pmReadoutSetWeights(pmReadout *readout)
+{
+
+    pmCell *cell = readout->parent;
+
+    float gain = psMetadataLookupF32(NULL, cell->concepts, "CELL.GAIN"); // Cell gain
+    float readnoise = psMetadataLookupF32(NULL, cell->concepts, "CELL.READNOISE"); // Cell read noise
+    psRegion *trimsec = psMetadataLookupPtr(NULL, cell->concepts, "CELL.TRIMSEC"); // Trim section
+    float saturation = psMetadataLookupF32(NULL, cell->concepts, "CELL.SATURATION"); // Saturation level
+    float bad = psMetadataLookupF32(NULL, cell->concepts, "CELL.BAD"); // Bad level
+
+    pmHDU *hdu = pmHDUFromCell (cell);
+
+    psArray *pixels = hdu->images;      // Array of images
+    psArray *weights = hdu->weights;    // Array of weight images
+    psArray *masks = hdu->masks;        // Array of mask images
+    // Generate the weights and masks if required
+    if (! weights) {
+        weights = psArrayAlloc(pixels->n);
+        for (int i = 0; i < pixels->n; i++) {
+            psImage *image = pixels->data[i];
+            weights->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+            psImageInit(weights->data[i], 0.0);
+        }
+        hdu->weights = weights;
+    }
+    if (! masks) {
+        masks = psArrayAlloc(pixels->n);
+        for (int i = 0; i < pixels->n; i++) {
+            psImage *image = pixels->data[i];
+            masks->data[i] = psImageAlloc(image->numCols, image->numRows, PS_TYPE_U8);
+            psImageInit(masks->data[i], 0);
+        }
+        hdu->masks = masks;
+    }
+
+    // Set the pixels
+    psArray *readouts = cell->readouts; // Array of readouts
+    for (int i = 0; i < readouts->n; i++) {
+        pmReadout *readout = readouts->data[i]; // The readout of interest
+
+        if (! readout->weight) {
+            readout->weight = psMemIncrRefCounter(psImageSubset(weights->data[i], *trimsec));
+        }
+        if (! readout->mask) {
+            readout->mask = psMemIncrRefCounter(psImageSubset(masks->data[i], *trimsec));
+        }
+
+        // Set up the mask
+        psImage *image = readout->image;// Pixels
+        psImage *mask = readout->mask;  // Mask image
+        for (int i = 0; i < image->numRows; i++) {
+            for (int j = 0; j < image->numCols; j++) {
+                if (image->data.F32[i][j] >= saturation) {
+                    mask->data.U8[i][j] = PM_MASK_SAT;
+                }
+                if (image->data.F32[i][j] <= bad) {
+                    mask->data.U8[i][j] = PM_MASK_BAD;
+                }
+            }
+        }
+
+        psImage *weight = readout->weight;  // Mask image
+        float rnoise = PS_SQR(readnoise/gain);
+        for (int i = 0; i < image->numRows; i++) {
+            for (int j = 0; j < image->numCols; j++) {
+                if (!mask->data.U8[i][j]) {
+                    weight->data.F32[i][j] = image->data.F32[i][j]/ gain + rnoise;
+                }
+            }
+        }
+
+        // Set weight image to the variance = g*f + rn^2
+        // psBinaryOp(readout->weight, image, "/", psScalarAlloc(gain, PS_TYPE_F32));
+        // psBinaryOp(readout->weight, readout->weight, "+",
+        // psScalarAlloc(readnoise*readnoise/gain/gain, PS_TYPE_F32));
+    }
+
+    return true;
+}
+
Index: trunk/psModules/src/astrom/pmReadout.h
===================================================================
--- trunk/psModules/src/astrom/pmReadout.h	(revision 6872)
+++ trunk/psModules/src/astrom/pmReadout.h	(revision 6872)
@@ -0,0 +1,13 @@
+#ifndef PM_READOUT_H
+#define PM_READOUT_H
+
+#include "pslib.h"
+#include "pmFPA.h"
+
+// Get the bias sections for a specific readout
+psList *pmReadoutGetBias(pmReadout *readout // Readout for which to get the bias sections
+                        );
+
+bool pmReadoutSetWeights(pmReadout *readout);
+
+#endif
Index: trunk/psModules/src/camera/Makefile.am
===================================================================
--- trunk/psModules/src/camera/Makefile.am	(revision 6529)
+++ trunk/psModules/src/camera/Makefile.am	(revision 6872)
@@ -3,7 +3,20 @@
 libpsmodulecamera_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
 libpsmodulecamera_la_LDFLAGS  = -release $(PACKAGE_VERSION)
-libpsmodulecamera_la_SOURCES  =
+libpsmodulecamera_la_SOURCES  = \
+	pmChipMosaic.c \
+	pmFPAConceptsGet.c \
+	pmFPAConceptsSet.c \
+	pmFPAConstruct.c \
+	pmFPAMorph.c \
+	pmFPARead.c \
+	pmFPAWrite.c
 
 psmoduleincludedir = $(includedir)
-psmoduleinclude_HEADERS =
-
+psmoduleinclude_HEADERS = \
+	pmChipMosaic.h \
+	pmFPAConceptsGet.h \
+	pmFPAConceptsSet.h \
+	pmFPAConstruct.h \
+	pmFPAMorph.h \
+	pmFPARead.h \
+	pmFPAWrite.h
Index: trunk/psModules/src/config/pmConfig.h
===================================================================
--- trunk/psModules/src/config/pmConfig.h	(revision 6529)
+++ trunk/psModules/src/config/pmConfig.h	(revision 6872)
@@ -3,6 +3,6 @@
  *  @author PAP, IfA
  *
- *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2006-02-02 04:51:14 $
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -15,7 +15,24 @@
 
 
+// Configuration information
+typedef struct
+{
+    psMetadata *site;                   // Site configuration
+    psMetadata *camera;                 // Camera specification
+    psMetadata *recipes;                // Recipes for processing
+    psMetadata *arguments;              // Command-line arguments
+    psMetadata *files;                  // pmFPAfiles used for analysis
+    //    psMetadata *format;   // camera format for primary image
+    psDB *database;                     // Database handle
+}
+pmConfig;
+
+pmConfig *pmConfigAlloc(void);
+
+// Free static variables
+void pmConfigDone(void);
 
 /** pmConfigRead
- * 
+ *
  * pmConfigRead shall load the site configuration (according to the above rule
  * for determining the source). The camera configuration shall also be loaded if
@@ -32,62 +49,57 @@
  *
  */
-bool pmConfigRead(
-    psMetadata **site,
-    psMetadata **camera,
-    psMetadata **recipe,
+pmConfig *pmConfigRead(
     int *argc,
-    char **argv,
-    const char *recipeName
+    char **argv);
+
+/** we need this elsewhere; make it public **/
+bool readConfig(
+    psMetadata **config,                // Config to output
+    const char *name,                   // Name of file
+    const char *description             // Description of file
 );
 
-
-
 /** pmConfigValidateCamera
- * 
+ *
  * This function, used by pmConfigCameraFromHeader, shall return true if the
  * FITS header matches the rule contained in the camera configuration (see
  * x2.2.2.3); otherwise it shall return false.
- * 
+ *
  */
-bool pmConfigValidateCamera(
-    const psMetadata *camera,
-    const psMetadata *header
-);
-
+bool pmConfigValidateCameraFormat(
+    const psMetadata *cameraFormat,
+    const psMetadata *header);
 
 
 /** pmConfigCameraFromHeader
- * 
+ *
  * pmConfigCameraFromHeader shall load the camera configuration based on the
  * contents of the FITS header, using the list of known cameras contained in the
  * site configuration. If more than one camera matches the FITS header, a warning
  * shall be generated and the first matching camera returned.
- * 
+ *
  */
-psMetadata *pmConfigCameraFromHeader(
-    const psMetadata *site,
-    const psMetadata *header
+psMetadata *pmConfigCameraFormatFromHeader(
+    pmConfig *config,                   // The configuration
+    const psMetadata *header           // The FITS header
 );
 
 
-
 /** pmConfigRecipeFromCamera
- * 
- * pmConfigRecipeFromCamera shall load the recipe configuration based on the
- * recipeName and the list of known recipes contained in the camera 
+ *
+ * pmConfigRecipeFromCamera shall load the recipes from the list of known recipes contained in the camera
  * configuration.
- * 
+ *
  */
-psMetadata *pmConfigRecipeFromCamera(
-    const psMetadata *camera,
-    const char *recipeName
+bool pmConfigReadRecipes(
+    pmConfig *config
 );
 
 /** pmConfigDB
- * 
+ *
  * pmConfigDB shall use the site configuration data to open a database handle.
  * This is fairly straightforward at the moment, but will change when we beef up
  * security. (TBD)
- * 
+ *
  */
 #ifdef DOMIT_PSDB
@@ -99,4 +111,16 @@
 );
 
+/** pmConfigConformHeader
+ *
+ * Make the supplied header conform to the nominated camera format.
+ */
+bool pmConfigConformHeader(psMetadata *header, // Header to conform
+                           const psMetadata *format // Camera format
+                          );
+
+
+psArray *pmConfigFileSets (int *argc, char **argv, char *file, char *list);
+bool pmConfigFileSetsMD (psMetadata *metadata, int *argc, char **argv, char *name, char *file, char *list);
+
 
 #endif
Index: trunk/psModules/src/detrend/Makefile.am
===================================================================
--- trunk/psModules/src/detrend/Makefile.am	(revision 6529)
+++ trunk/psModules/src/detrend/Makefile.am	(revision 6872)
@@ -3,15 +3,18 @@
 libpsmoduledetrend_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
 libpsmoduledetrend_la_LDFLAGS  = -release $(PACKAGE_VERSION)
-libpsmoduledetrend_la_SOURCES  = pmFlatField.c \
-    pmMaskBadPixels.c \
-    pmNonLinear.c
+libpsmoduledetrend_la_SOURCES  = \
+	pmFlatField.c \
+	pmFringeStats.c \
+	pmMaskBadPixels.c \
+	pmNonLinear.c
 
 psmoduleincludedir = $(includedir)
 psmoduleinclude_HEADERS = \
-  pmFlatField.h \
-  pmFlatFieldErrors.h \
-  pmMaskBadPixelsErrors.h \
-  pmMaskBadPixels.h \
-  pmNonLinear.h
+	pmFlatField.h \
+	pmFlatFieldErrors.h \
+	pmFringeStats.h \
+	pmMaskBadPixelsErrors.h \
+	pmMaskBadPixels.h \
+	pmNonLinear.h
 
 EXTRA_DIST = pmFlatFieldErrors.dat pmMaskBadPixelsErrors.dat
Index: trunk/psModules/src/detrend/pmFlatField.c
===================================================================
--- trunk/psModules/src/detrend/pmFlatField.c	(revision 6529)
+++ trunk/psModules/src/detrend/pmFlatField.c	(revision 6872)
@@ -1,2 +1,8 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
 /** @file  pmFlatField.c
  *
@@ -18,6 +24,6 @@
  *  @author Ross Harman, MHPCC
  *
- *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-11-18 19:43:14 $
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -30,5 +36,5 @@
 #include<stdio.h>
 #include<math.h>
-#include <string.h>
+#include <strings.h>
 
 #include "pslib.h"
@@ -36,32 +42,8 @@
 #include "pmMaskBadPixels.h"
 #include "pmFlatFieldErrors.h"
-#include "pmSubtractBias.h"
 
-// XXX: This should be removed when the autoconf stuff handles psConstants.h correctly.
-#define PS_WARN_PTR_NON_NULL(NAME) \
-if ((NAME) == NULL) { \
-    psLogMsg(__func__, PS_LOG_WARN, "WARNING: %s is NULL.", #NAME); \
-} \
 
-bool pmFlatField(
-    pmReadout *in,
-    const pmReadout *flat)
+bool pmFlatField(pmReadout *in, const pmReadout *flat)
 {
-    // XXX: Use the proper image and readout asserts.
-    PS_ASSERT_PTR_NON_NULL(in, true);
-    PS_ASSERT_PTR_NON_NULL(in->image, false);
-    PS_ASSERT_PTR_NON_NULL(in->mask, false);
-    PS_ASSERT_PTR_NON_NULL(flat, false);
-    PS_ASSERT_PTR_NON_NULL(flat->image, false);
-    if (in == NULL)
-        printf("XXX: NULL\n");
-
-    // XXX: Not sure if this is correct.  Must consult with IfA.
-    PS_ASSERT_PTR_NON_NULL(in->mask, false);
-
-    PS_WARN_PTR_NON_NULL(in->parent);
-    if (in->parent != NULL) {
-        PS_WARN_PTR_NON_NULL(in->parent->concepts);
-    }
     int i = 0;
     int j = 0;
@@ -71,28 +53,43 @@
     psElemType flatType;
     psElemType maskType;
-    psImage *inMask = NULL;
-    psImage *flatImage = NULL;
 
-    //
-    // Determine trimmed image from metadata.
-    //
-    psImage *trimmedImg = p_psDetermineTrimmedImage(in);
-    flatImage = flat->image;
-    inMask = in->mask;
+    // 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;
+    }
+
+    psImage *inImage   = in->image;     // Input image
+    psImage *inMask    = in->mask;      // Mask for input image
+    psImage *flatImage = flat->image;   // Flat-field image
+
+    // Offsets on the chip
+    int x0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.X0");
+    int y0in = psMetadataLookupS32(NULL, in->parent->concepts, "CELL.Y0");
+    int x0flat = psMetadataLookupS32(NULL, flat->parent->concepts, "CELL.X0");
+    int y0flat = psMetadataLookupS32(NULL, flat->parent->concepts, "CELL.Y0");
+
+    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;
+    }
 
     // Check input image and its mask are not larger than flat image
 
-    if (trimmedImg == NULL)
-        printf("XXX: 00\n");
-    if (flatImage == NULL)
-        printf("XXX 01\n");
-
-    if (trimmedImg->numRows>flatImage->numRows || trimmedImg->numCols>flatImage->numCols) {
+    if (inImage->numRows>flatImage->numRows || inImage->numCols>flatImage->numCols) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmFlatField_SIZE_INPUT_IMAGE,
-                 trimmedImg->numRows, trimmedImg->numCols, flatImage->numRows, flatImage->numCols);
+                 inImage->numRows, inImage->numCols, flatImage->numRows, flatImage->numCols);
         return false;
     }
-    if (inMask->numRows > flatImage->numRows || inMask->numCols > flatImage->numCols) {
+    if (inMask && (inMask->numRows > flatImage->numRows || inMask->numCols > flatImage->numCols)) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmFlatField_SIZE_MASK_IMAGE,
@@ -102,6 +99,6 @@
 
     // Determine total offset based on image offset with chip offset
-    totOffCol = trimmedImg->col0 + in->col0;
-    totOffRow = trimmedImg->row0 + in->row0;
+    totOffCol = inImage->col0 + y0in - flatImage->col0 - y0flat;
+    totOffRow = inImage->row0 + x0in - flatImage->row0 - x0flat;
 
     // Check that offsets are within image limits
@@ -111,10 +108,10 @@
                  totOffRow, totOffCol, flatImage->numRows, flatImage->numCols);
         return false;
-    } else if(totOffRow>=trimmedImg->numRows || totOffCol>=trimmedImg->numCols) {
+    } else if(totOffRow>=inImage->numRows || totOffCol>=inImage->numCols) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmFlatField_OFFSET_INPUT_IMAGE,
-                 totOffRow, totOffCol, trimmedImg->numRows, trimmedImg->numCols);
+                 totOffRow, totOffCol, inImage->numRows, inImage->numCols);
         return false;
-    } else if(totOffRow>=inMask->numRows || totOffCol>=inMask->numCols) {
+    } else if(inMask && (totOffRow>=inMask->numRows || totOffCol>=inMask->numCols)) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmFlatField_OFFSET_MASK_IMAGE,
@@ -124,5 +121,5 @@
 
     // Check for incorrect types
-    inType = trimmedImg->type.type;
+    inType = inImage->type.type;
     flatType = flatImage->type.type;
     maskType = inMask->type.type;
@@ -137,5 +134,5 @@
                  flatType);
         return false;
-    } else if(maskType != PS_TYPE_MASK) {
+    } else if(inMask && inMask->type.type != PS_TYPE_MASK) {
         psError( PS_ERR_BAD_PARAMETER_TYPE, true,
                  PS_ERRORTEXT_pmFlatField_TYPE_MASK_IMAGE,
@@ -153,18 +150,20 @@
 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 < trimmedImg->numRows; j++) {                                                          \
-        for(i = totOffCol; i < trimmedImg->numCols; i++) {                                                      \
+    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;                                        \
+                if (inMask) {                                                                                \
+                    inMask->data.PS_TYPE_MASK_DATA[j][i] |= PM_MASK_FLAT;                                    \
+                }                                                                                            \
                 flatImage->data.TYPE[j][i] = 0.0;                                                            \
             }                                                                                                \
         }                                                                                                    \
     }                                                                                                        \
-    for(j = totOffRow; j < trimmedImg->numRows; j++) {                                                          \
-        for(i = totOffCol; i < trimmedImg->numCols; i++) {                                                      \
-            if(!inMask->data.PS_TYPE_MASK_DATA[j][i]) {                                                      \
+    for(j = totOffRow; j < inImage->numRows; j++) {                                                          \
+        for(i = totOffCol; i < inImage->numCols; i++) {                                                      \
+            if(inMask && !inMask->data.PS_TYPE_MASK_DATA[j][i]) {                                            \
                 /* Module shall divide the input image by the flat-fielded image */                          \
-                trimmedImg->data.TYPE[j][i] /= flatImage->data.TYPE[j][i];                                      \
+                inImage->data.TYPE[j][i] /= flatImage->data.TYPE[j][i];                                      \
             }                                                                                                \
         }                                                                                                    \
Index: trunk/psModules/src/detrend/pmFlatField.h
===================================================================
--- trunk/psModules/src/detrend/pmFlatField.h	(revision 6529)
+++ trunk/psModules/src/detrend/pmFlatField.h	(revision 6872)
@@ -1,2 +1,8 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
 /** @file  pmFlatField.h
  *
@@ -18,6 +24,6 @@
  *  @author Ross Harman, MHPCC
  *
- *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-10-20 23:06:24 $
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -25,5 +31,5 @@
 
 #include "pslib.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 
 
Index: trunk/psModules/src/detrend/pmFringeStats.c
===================================================================
--- trunk/psModules/src/detrend/pmFringeStats.c	(revision 6872)
+++ trunk/psModules/src/detrend/pmFringeStats.c	(revision 6872)
@@ -0,0 +1,240 @@
+/** @file  pmFringeStats.c
+ *
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 IfA
+ */
+
+#include "pslib.h"
+#include "pmFPA.h"
+#include "pmFringeStats.h"
+
+static void fringeRegionsFree(pmFringeRegions *fringe)
+{
+    psFree(fringe->x);
+    psFree(fringe->y);
+    psFree(fringe->mask);
+    return;
+}
+
+pmFringeRegions *pmFringeRegionsAlloc(int nPts, int dX, int dY, int nX, int nY)
+{
+    pmFringeRegions *fringe = psAlloc(sizeof(pmFringeRegions));
+    (void)psMemSetDeallocator(fringe, (psFreeFunc)fringeRegionsFree);
+
+    fringe->x = NULL;
+    fringe->y = NULL;
+    fringe->mask = NULL;
+
+    fringe->nRequested = nPts;
+    fringe->nAccepted = 0;
+
+    fringe->dX = dX;
+    fringe->dY = dY;
+    fringe->nX = nX;
+    fringe->nY = nY;
+
+    return fringe;
+}
+
+
+static void fringeStatsFree(pmFringeStats *stats)
+{
+    psFree(stats->regions);
+    psFree(stats->f);
+    psFree(stats->df);
+}
+
+pmFringeStats *pmFringeStatsAlloc(pmFringeRegions *regions)
+{
+    pmFringeStats *stats = psAlloc(sizeof(pmFringeStats));
+    (void)psMemSetDeallocator(stats, (psFreeFunc)fringeStatsFree);
+
+    stats->regions = psMemIncrRefCounter(regions);
+    stats->f = psVectorAlloc(regions->x->n, PS_TYPE_F32);
+    stats->df = psVectorAlloc(regions->x->n, PS_TYPE_F32);
+
+    return stats;
+}
+
+
+static void fringeScaleFree(pmFringeScale *scale)
+{
+    psFree(scale->coeff);
+    psFree(scale->coeffErr);
+    return;
+}
+
+pmFringeScale *pmFringeScaleAlloc(int nFringeFrames)
+{
+    pmFringeScale *scale = psAlloc(sizeof(pmFringeScale));
+    (void)psMemSetDeallocator(scale, (psFreeFunc)fringeScaleFree);
+
+    scale->nFringeFrames = nFringeFrames;
+    scale->coeff = psVectorAlloc(nFringeFrames + 1, PS_TYPE_F32);
+    scale->coeffErr = psVectorAlloc(nFringeFrames + 1, PS_TYPE_F32);
+
+    return scale;
+}
+
+bool pmFringeStatsCreatePoints(pmFringeRegions *fringe, psImage *image)
+{
+
+    double frnd;
+    // create fringe->nRequested
+
+    psRandom *rnd = psRandomAlloc(PS_RANDOM_TAUS, 0);
+
+    fringe->x = psVectorRecycle(fringe->x, fringe->nRequested, PS_TYPE_F32);
+    fringe->y = psVectorRecycle(fringe->y, fringe->nRequested, PS_TYPE_F32);
+    fringe->mask = psVectorRecycle(fringe->mask, fringe->nRequested, PS_TYPE_U8);
+
+    int nX = image->numCols;
+    int nY = image->numRows;
+
+    psF32 *xPt = fringe->x->data.F32;
+    psF32 *yPt = fringe->y->data.F32;
+
+    int dX = fringe->dX;
+    int dY = fringe->dY;
+
+    // generate random points located within image bounds
+    for (int i = 0; i < fringe->nRequested; i++) {
+        frnd = psRandomUniform(rnd);
+        xPt[i] = (nX - 2*dX)* frnd + dX;
+        frnd = psRandomUniform(rnd);
+        yPt[i] = (nY - 2*dY)* frnd + dY;
+    }
+    return true;
+}
+
+pmFringeStats *pmFringeStatsMeasure(pmFringeRegions *fringe, pmReadout *readout, psMaskType maskVal)
+{
+    if (!fringe->x || !fringe->y) {
+        // create the fringe vectors for this image
+        pmFringeStatsCreatePoints(fringe, readout->image);
+    }
+
+    PS_ASSERT_PTR_NON_NULL(fringe->x, false);
+    PS_ASSERT_PTR_NON_NULL(fringe->y, false);
+
+    pmFringeStats *measurements = pmFringeStatsAlloc(fringe);
+
+    psF32 *xPt = fringe->x->data.F32;
+    psF32 *yPt = fringe->y->data.F32;
+    psF32 *fPt = measurements->f->data.F32;
+    psF32 *dfPt = measurements->df->data.F32;
+
+    int dX = fringe->dX;
+    int dY = fringe->dY;
+
+    psImage *image = readout->image;
+    psImage *mask  = readout->mask;
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    for (int i = 0; i < fringe->x->n; i++) {
+        psRegion region = psRegionSet(xPt[i] - dX, yPt[i] - dY, xPt[i] + dX + 1, yPt[i] + dY + 1);
+        region = psRegionForImage(image, region);
+        psImage *subImage = psImageSubset(image, region);
+        psImage *subMask = psImageSubset(mask, region);
+        psImageStats(stats, subImage, subMask, maskVal);
+
+        fPt[i] = stats->sampleMedian;
+        dfPt[i] = stats->sampleStdev;
+    }
+
+    return measurements;
+}
+
+// XXX include the fringe error (fringe->df) in the fit?
+pmFringeScale *pmFringeScaleMeasure(pmFringeStats *science, psArray *fringes)
+{
+    double sum;
+    psVector *v1;
+    psVector *v2;
+
+    pmFringeScale *scale = pmFringeScaleAlloc(fringes->n);
+
+    int nCof = fringes->n + 1;
+    int nPts = science->f->n;
+
+    psImage *A = psImageAlloc(nCof, nCof, PS_TYPE_F64);
+    psVector *B = psVectorAlloc(nCof, PS_TYPE_F64);
+
+    for (int i = 0; i < nCof; i++) {
+        psF32 *p1 = NULL;
+        if (i != 0) {
+            v1 = fringes->data[i - 1];
+            p1 = v1->data.F32;
+        }
+        for (int j = 0; j < nCof; j++) {
+            psF32 *p2 = NULL;
+            if (j != 0) {
+                v2 = fringes->data[j - 1];
+                p2 = v2->data.F32;
+            }
+
+            sum = 0;
+            for (int k = 0; k < nPts; k++) {
+                psF32 f1 = (p1 == NULL) ? 1.0 : p1[k];
+                psF32 f2 = (p2 == NULL) ? 1.0 : p2[k];
+                sum += f1*f2;
+            }
+            A->data.F32[i][j] = sum;
+        }
+
+        sum = 0;
+        psF32 *p2 = science->f->data.F32;
+        for (int j = 0; j < nPts; j++) {
+            psF32 f1 = (p1 == NULL) ? 1.0 : p1[j];
+            psF32 f2 = p2[j];
+            sum += f1*f2;
+        }
+        B->data.F32[i] = sum;
+    }
+
+    if (! psGaussJordan(A, B)) {
+        psError(PS_ERR_UNKNOWN, false, "Could not solve linear equations.  Returning NULL.\n");
+    }
+
+
+    for (int i = 0; i < nCof; i++) {
+        scale->coeff->data.F32[i] = B->data.F32[i];
+        scale->coeffErr->data.F32[i] = sqrt(A->data.F32[i][i]);
+    }
+
+    return scale;
+}
+
+// XXX note that this modifies the input fringe images
+psImage *pmFringeCorrect(pmReadout *readout, pmFringeRegions *fringes, psArray *fringeImages, psArray *fringeStats, psMaskType maskVal)
+{
+    // measure the fringe stats for the science frame and solve for the scales
+    pmFringeStats *scienceStats = pmFringeStatsMeasure(fringes, readout, maskVal);
+    pmFringeScale *scale = pmFringeScaleMeasure(scienceStats, fringeStats);
+    psFree(scienceStats);
+
+    // build the fringe correction image
+    // XXX we could save data space by making the first image the output image
+    psImage *sumFringe = NULL;
+    for (int i = 0; i < fringeImages->n; i++) {
+
+        // rescale the fringe image
+        psBinaryOp(fringeImages->data[i], fringeImages->data[i], "*",
+                   psScalarAlloc(scale->coeff->data.F32[i+1], PS_TYPE_F32));
+
+        // sum together
+        sumFringe = (psImage*)psBinaryOp(sumFringe, sumFringe, "+", fringeImages->data[i]);
+    }
+    psFree(scale);
+
+    // subtract the resulting fringe frame
+    readout->image = (psImage*)psBinaryOp(readout->image, readout->image, "-", sumFringe);
+
+    return sumFringe;
+}
+
Index: trunk/psModules/src/detrend/pmFringeStats.h
===================================================================
--- trunk/psModules/src/detrend/pmFringeStats.h	(revision 6872)
+++ trunk/psModules/src/detrend/pmFringeStats.h	(revision 6872)
@@ -0,0 +1,92 @@
+/** @file  pmFringeStats.h
+ *
+ *  @brief Measure Fringe statistics and apply correction
+ *
+ *  @author Eugene Magnier, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 IfA, University of Hawaii
+ */
+
+# ifndef PM_FRINGE_STATS
+# define PM_FRINGE_STATS
+
+/** Structure to hold the fringe measurement regions.
+ */
+typedef struct
+{
+    psU32 nRequested;   // number of fringe points selected
+    psU32 nAccepted;   // number of fringe points not masked
+    psU32 dX;    // median box half-width
+    psU32 dY;    // median box half-height
+    psU32 nX;    // large-scale smoothing in x (col)
+    psU32 nY;    // large-scale smoothing in y (row)
+    psVector *x;    // fringe point coordinates (col)
+    psVector *y;    // fringe point coordinates (row)
+    psVector *mask;   // fringe point on/off mask
+}
+pmFringeRegions;
+
+pmFringeRegions *pmFringeRegionsAlloc (
+    int nPts,     // number of points to create
+    int dX,     // half-width of fringe boxes
+    int dY,     // half-height of fringe boxes
+    int nX,     // smoothing scale in x
+    int nY    // smoothing scale in y
+);
+
+/** Structure to hold the fringe measurements for a particular image
+ */
+typedef struct
+{
+    pmFringeRegions *regions;           // Fringe regions
+    psVector *f;    // fringe point median
+    psVector *df;   // fringe point stdev
+}
+pmFringeStats;
+
+pmFringeStats *pmFringeStatsAlloc(pmFringeRegions *regions);
+
+
+/** the pmFringeScale structure defines the relationship between two fringe measurements
+ */
+typedef struct
+{
+    int nFringeFrames;
+    psVector *coeff;
+    psVector *coeffErr;
+}
+pmFringeScale;
+
+/** Measure the fringe stats for an image
+ *
+ *  Given an input image and a fringe stats structure, measure the statistics for each of the fringe points on
+ *  the image.  If the fringe vectors are NULL, new vectors are created.
+ *
+ *  XXX should the structure carry the image dimensions for validation?
+ *
+ *  @return  bool: True or false for success or failure
+ */
+pmFringeStats *pmFringeStatsMeasure(
+    pmFringeRegions *fringe,            ///< fringe regions
+    pmReadout *readout,                 ///< measure fringes on this readout
+    psMaskType maskVal                  ///< Mask value for statistics
+);
+
+/** Fringe correct the science image
+ *
+ *  Given a science image, a set of fringe stats and a matched set of fringe images, correct the science image
+ *  for the fringe images.
+ *
+ *  @return  psImage: summed fringe image
+ */
+psImage *pmFringeCorrect(pmReadout *in, // place info about results here
+                         pmFringeRegions *fringes, // The fringe regions used
+                         psArray *fringeImages, // fringe images to use in correction
+                         psArray *fringeStats, // fringe stats to use in correction
+                         psMaskType maskVal // Value to mask
+                        );
+
+# endif
Index: trunk/psModules/src/detrend/pmMaskBadPixels.c
===================================================================
--- trunk/psModules/src/detrend/pmMaskBadPixels.c	(revision 6529)
+++ trunk/psModules/src/detrend/pmMaskBadPixels.c	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmMaskBadPixels.c
  *
@@ -19,6 +24,6 @@
  *  @author Ross Harman, MHPCC
  *
- *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-11-18 19:43:14 $
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -31,157 +36,77 @@
 #include<stdio.h>
 #include<math.h>
-#include<string.h>
+#include<strings.h>
 
 #include "pmMaskBadPixels.h"
 #include "pmMaskBadPixelsErrors.h"
-#include "pmSubtractBias.h"
 
-//XXX: REmove, autoconf is broken.
-#define PS_WARN_PTR_NON_NULL(NAME) \
-if ((NAME) == NULL) { \
-    psLogMsg(__func__, PS_LOG_WARN, "WARNING: %s is NULL.", #NAME); \
-} \
+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;
 
 
-/******************************************************************************
-GrowPixel(inMask, row, col, radius, maskVal): This private routine takes an
-input image mask and a pixel location, then sets (logical or) all pixels with
-parameter radius if that pixel to maskVal parameter.
-*****************************************************************************/
-psBool GrowPixel(
-    psImage *inMask,
-    psS32 col,
-    psS32 row,
-    psS32 radius,
-    psU32 maskVal)
-{
-    psS32 rowMin = PS_MAX(row-radius, 0);
-    psS32 rowMax = PS_MIN(row+radius+1, inMask->numRows);
-    psS32 colMin = PS_MAX(col-radius, 0);
-    psS32 colMax = PS_MIN(col+radius+1, inMask->numCols);
-    psF32 squareRadius = PS_SQR(radius);
+    // 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;
+    }
 
-
-    for(psS32 i=rowMin; i<rowMax; i++) {
-        for(psS32 j=colMin; j<colMax; j++) {
-            // Old code:
-            //            psF32 rRound = 0.5 + sqrtf((psF32) (((col-j)*(col-j)) + ((row-i)*(row-i))));
-            //            if(rRound <= radius) {
-            psF32 squareDis = (psF32) (((col-j)*(col-j)) + ((row-i)*(row-i)));
-            if (squareDis <= squareRadius) {
-                inMask->data.U8[i][j] |= maskVal;
-            }
-        }
-    }
-    return(true);
-}
-
-/******************************************************************************
-GetRadius(inImg, col, row, sat, growVal, grow): This private routine takes an
-input image and pixel location and determines what radius that pixel must grow
-by.
- 
-//XXX: Inline this or macro it for speed.
- *****************************************************************************/
-static psS32 GetRadius(
-    psImage *inImg,
-    psS32 col,
-    psS32 row,
-    psF32 sat,
-    psU32 growVal,
-    psS32 grow)
-{
-    psS32 growRadius = 0;
-    if (inImg->type.type == PS_TYPE_F32) {
-        if(inImg->data.F32[row][col] == growVal) {
-            growRadius = grow;
-        }
-        if (inImg->data.F32[row][col] > sat) {
-            growRadius = PS_MAX(growRadius+1, 2);
-        }
-    } else if (inImg->type.type == PS_TYPE_S32) {
-        if(inImg->data.S32[row][col] == growVal) {
-            growRadius = grow;
-        }
-        if (inImg->data.S32[row][col] > sat) {
-            growRadius = PS_MAX(growRadius+1, 2);
-        }
-    } else if (inImg->type.type == PS_TYPE_U16) {
-        if(inImg->data.U16[row][col] == growVal) {
-            growRadius = grow;
-        }
-        if (inImg->data.U16[row][col] > sat) {
-            growRadius = PS_MAX(growRadius+1, 2);
-        }
-    } else {
-        psError( PS_ERR_BAD_PARAMETER_TYPE, true,
-                 PS_ERRORTEXT_pmMaskBadPixels_TYPE_MASK_IMAGE,
-                 inImg->type.type);
-    }
-    return(growRadius);
-}
-
-
-bool pmMaskBadPixels(
-    pmReadout *in,
-    const pmReadout *mask,
-    unsigned int maskVal,
-    float sat,
-    unsigned int growVal,
-    int grow)
-{
-    // XXX: Review this code then put all asserts at the top.
-    PS_ASSERT_PTR_NON_NULL(in, true);
-    PS_ASSERT_PTR_NON_NULL(in->image, false);
-    PS_WARN_PTR_NON_NULL(in->parent);
-    if (in->parent != NULL) {
-        PS_WARN_PTR_NON_NULL(in->parent->concepts);
-    }
-    PS_ASSERT_PTR_NON_NULL(mask, false);
-    int i = 0;
-    int j = 0;
-    int totOffCol = 0;
-    int totOffRow = 0;
-    psElemType inType;
-    psElemType maskType;
-    psImage *inMask = NULL;
-
-    //
-    // Determine trimmed image from metadata.
-    //
-    psImage *trimmedImg = p_psDetermineTrimmedImage(in);
-    if(in->mask == NULL) {
-        in->mask = psImageAlloc(trimmedImg->numCols, trimmedImg->numRows, PS_TYPE_MASK);
-        PS_IMAGE_SET_U8(in->mask, 0);
+    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(trimmedImg->numRows > mask->image->numRows || trimmedImg->numCols > mask->image->numCols) {
+    if(inImage->numRows > mask->numRows || inImage->numCols > mask->numCols) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmMaskBadPixels_SIZE_INPUT_IMAGE,
-                 trimmedImg->numRows, trimmedImg->numCols, mask->image->numRows, mask->image->numCols);
+                 inImage->numRows, inImage->numCols, mask->numRows, mask->numCols);
         return false;
-    } else if(inMask->numRows > mask->image->numRows || inMask->numCols > mask->image->numCols) {
+    } 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->image->numRows, mask->image->numCols);
+                 inMask->numRows, inMask->numCols, mask->numRows, mask->numCols);
         return false;
     }
 
     // Determine total offset based on image offset with chip offset
-    totOffCol = trimmedImg->col0 + in->col0;
-    totOffRow = trimmedImg->row0 + in->row0;
+    totOffCol = inImage->col0; // + in->col0;
+    totOffRow = inImage->row0; // + in->row0;
 
     // Check that offsets are within image limits
-    if(totOffRow>=mask->image->numRows || totOffCol>=mask->image->numCols) {
+    if(totOffRow>=mask->numRows || totOffCol>=mask->numCols) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmMaskBadPixels_OFFSET_MASK_IMAGE,
-                 totOffRow, totOffCol, mask->image->numRows, mask->image->numCols);
+                 totOffRow, totOffCol, mask->numRows, mask->numCols);
         return false;
-    } else if(totOffRow>=trimmedImg->numRows || totOffCol>=trimmedImg->numCols) {
+    } else if(totOffRow>=inImage->numRows || totOffCol>=inImage->numCols) {
         psError( PS_ERR_BAD_PARAMETER_SIZE, true,
                  PS_ERRORTEXT_pmMaskBadPixels_OFFSET_INPUT_IMAGE,
-                 totOffRow, totOffCol, trimmedImg->numRows, trimmedImg->numCols);
+                 totOffRow, totOffCol, inImage->numRows, inImage->numCols);
         return false;
     } else if(totOffRow>=inMask->numRows || totOffCol>=inMask->numCols) {
@@ -193,6 +118,6 @@
 
     // Check for incorrect types
-    inType = trimmedImg->type.type;
-    maskType = mask->image->type.type;
+    inType = inImage->type.type;
+    maskType = mask->type.type;
     if(PS_IS_PSELEMTYPE_COMPLEX(inType)) {
         psError( PS_ERR_BAD_PARAMETER_TYPE, true,
@@ -207,34 +132,57 @@
     }
 
-    //
-    // This is the main loop which examines each pixel and masks pixels if
-    //    A: The mask matches the maskValue
-    //    B: The pixel is larger than the saturation value
-    //    C: The pixel equals the grow value (in which case a circle is masked)
-    //
-    for(i=totOffRow; i<trimmedImg->numRows; i++) {
-        for(j=totOffCol; j<trimmedImg->numCols; j++) {
-            //
-            // A: Pixels which satisfy maskVal shall be masked
-            //
-            if (mask->image->data.U8[i][j] == maskVal) {
-                in->mask->data.U8[i][j] |= maskVal;
-            }
+    // 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;
 
-            //
-            // We first determine how much we need to grow by and store this in
-            // growRadius.
-            //
-            psS32 growRadius = GetRadius(trimmedImg, j, i, sat, growVal, grow);
-
-            //
-            // Grow the pixel mask if necessary.
-            //
-            if (growRadius != 0) {
-                GrowPixel(in->mask, j, i, growRadius, maskVal);
-            }
-        }
+    // 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 true;
+    return false;
 }
Index: trunk/psModules/src/detrend/pmMaskBadPixels.h
===================================================================
--- trunk/psModules/src/detrend/pmMaskBadPixels.h	(revision 6529)
+++ trunk/psModules/src/detrend/pmMaskBadPixels.h	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmMaskBadPixels.h
  *
@@ -19,6 +24,6 @@
  *  @author Ross Harman, MHPCC
  *
- *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-11-15 20:09:03 $
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -26,5 +31,5 @@
 
 #include "pslib.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 
 /** Mask values */
@@ -33,8 +38,8 @@
     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.
+    PM_MASK_BAD     = 0x0008,   ///< The pixel is low
+    PM_MASK_FLAT    = 0x0010    ///< The pixel is non-positive in the flat-field.
 } pmMaskValue;
 
-// XXX: Use PS_MIN, PS_MAX
 /** Macro to find maximum of two numbers */
 #define MAX(A,B)((A)>=(B)?(A):(B))
@@ -54,5 +59,5 @@
 bool pmMaskBadPixels(
     pmReadout *in,          ///< Readout containing input image data.
-    const pmReadout *mask,   ///< Mask data to be added to readout mask 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.
Index: trunk/psModules/src/detrend/pmMaskBadPixelsErrors.h
===================================================================
--- trunk/psModules/src/detrend/pmMaskBadPixelsErrors.h	(revision 6529)
+++ trunk/psModules/src/detrend/pmMaskBadPixelsErrors.h	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmMaskBadPixelsErrors.h
  *
@@ -7,6 +12,6 @@
  *  @author Ross Harman, MHPCC
  *
- *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-09-28 20:43:52 $
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -23,5 +28,5 @@
  *     $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.
  */
Index: trunk/psModules/src/detrend/pmNonLinear.c
===================================================================
--- trunk/psModules/src/detrend/pmNonLinear.c	(revision 6529)
+++ trunk/psModules/src/detrend/pmNonLinear.c	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmNonLinear.c
  *
@@ -5,6 +10,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-12-05 20:49:40 $
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -22,22 +27,14 @@
 
 #include "pmNonLinear.h"
-#include "pmSubtractBias.h"
 
-// XXX: Remove, autoconf must be
-#define PS_WARN_PTR_NON_NULL(NAME) \
-if ((NAME) == NULL) { \
-    psLogMsg(__func__, PS_LOG_WARN, "WARNING: %s is NULL.", #NAME); \
-} \
 /******************************************************************************
 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
+be evaluated at that pixels value, and the image pixel will then be set to
 that value.
-*****************************************************************************/
+ *****************************************************************************/
 
-pmReadout *pmNonLinearityPolynomial(
-    pmReadout *inputReadout,
-    const psPolynomial1D *input1DPoly)
+pmReadout *pmNonLinearityPolynomial(pmReadout *inputReadout,
+                                    const psPolynomial1D *input1DPoly)
 {
     PS_ASSERT_PTR_NON_NULL(inputReadout, NULL);
@@ -45,18 +42,11 @@
     PS_ASSERT_IMAGE_TYPE(inputReadout->image, PS_TYPE_F32, NULL);
     PS_ASSERT_PTR_NON_NULL(input1DPoly, NULL);
-    PS_WARN_PTR_NON_NULL(inputReadout->parent);
-    if (inputReadout->parent != NULL) {
-        PS_WARN_PTR_NON_NULL(inputReadout->parent->concepts);
-    }
 
-    //
-    // Determine trimmed image from metadata.
-    //
-    psImage *trimmedImg = p_psDetermineTrimmedImage(inputReadout);
+    psS32 i;
+    psS32 j;
 
-    for (psS32 i=0;i<trimmedImg->numRows;i++) {
-        for (psS32 j=0;j<trimmedImg->numCols;j++) {
-            trimmedImg->data.F32[i][j] = psPolynomial1DEval(input1DPoly,
-                                         trimmedImg->data.F32[i][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]);
         }
     }
@@ -71,48 +61,68 @@
 inFluxe, and the corresponding value in outFlux.  The image pixel will then
 be set to the value from outFlux.
- 
-XXX: Must assert that filename exists.  This should probably happen in
-the lookup files.
  *****************************************************************************/
-pmReadout *pmNonLinearityLookup(
-    pmReadout *inputReadout,
-    const char *filename
-)
+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_WARN_PTR_NON_NULL(inputReadout->parent);
-    if (inputReadout->parent != NULL) {
-        PS_WARN_PTR_NON_NULL(inputReadout->parent->concepts);
+    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);
     }
-    //
-    // Determine trimmed image from metadata.
-    //
-    psImage *trimmedImg = p_psDetermineTrimmedImage(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);
 
-    psLookupTable *tmpLT = psLookupTableAlloc(filename, "%f %f", 0);
-    psS32 numLines = psLookupTableRead(tmpLT);
+    psS32 i;
+    psS32 j;
+    psS32 binNum;
+    psScalar x;
     psS32 numPixels = 0;
-    if (numLines < 2) {
-        psLogMsg(__func__, PS_LOG_WARN,
-                 "WARNING: Lookup Table is too small.  Returning original pmReadout.\n");
-    } else {
-        for (psS32 i=0;i<trimmedImg->numRows;i++) {
-            for (psS32 j=0;j<trimmedImg->numCols;j++) {
-                psF64 tmpD = psLookupTableInterpolate(tmpLT, trimmedImg->data.F32[i][j], 1);
-                if (!isnan(tmpD)) {
-                    trimmedImg->data.F32[i][j] = tmpD;
-                } else {
-                    numPixels++;
-                }
+    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);
-        }
     }
-
+    if (numPixels > 0) {
+        psLogMsg(__func__, PS_LOG_WARN,
+                 "WARNING: pmNonLinear.c: pmNonLinearityLookup(): %d pixels outside table.", numPixels);
+    }
     return(inputReadout);
 }
Index: trunk/psModules/src/detrend/pmNonLinear.h
===================================================================
--- trunk/psModules/src/detrend/pmNonLinear.h	(revision 6529)
+++ trunk/psModules/src/detrend/pmNonLinear.h	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmNonLinear.h
  *
@@ -5,6 +10,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-10-20 23:06:24 $
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -16,15 +21,12 @@
 
 #include "pslib.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 
-pmReadout *pmNonLinearityPolynomial(
-    pmReadout *in,
-    const psPolynomial1D *coeff
-);
+pmReadout *pmNonLinearityPolynomial(pmReadout *in,
+                                    const psPolynomial1D *coeff);
 
-pmReadout *pmNonLinearityLookup(
-    pmReadout *in,
-    const char *filename
-);
+pmReadout *pmNonLinearityLookup(pmReadout *in,
+                                const psVector *inFlux,
+                                const psVector *outFlux);
 
 #endif
Index: trunk/psModules/src/imcombine/Makefile.am
===================================================================
--- trunk/psModules/src/imcombine/Makefile.am	(revision 6529)
+++ trunk/psModules/src/imcombine/Makefile.am	(revision 6872)
@@ -3,9 +3,9 @@
 libpsmoduleimcombine_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
 libpsmoduleimcombine_la_LDFLAGS  = -release $(PACKAGE_VERSION)
-libpsmoduleimcombine_la_SOURCES  = pmImageCombine.c \
-    pmReadoutCombine.c
+libpsmoduleimcombine_la_SOURCES  = pmImageCombine.c
+#    pmReadoutCombine.c
 
 psmoduleincludedir = $(includedir)
 psmoduleinclude_HEADERS = \
-    pmImageCombine.h \
-    pmReadoutCombine.h
+    pmImageCombine.h
+#    pmReadoutCombine.h
Index: trunk/psModules/src/imcombine/pmReadoutCombine.h
===================================================================
--- trunk/psModules/src/imcombine/pmReadoutCombine.h	(revision 6529)
+++ trunk/psModules/src/imcombine/pmReadoutCombine.h	(revision 6872)
@@ -5,6 +5,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2006-01-26 21:10:50 $
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -23,5 +23,5 @@
 #include "pslib.h"
 #include "psConstants.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 
 typedef struct
@@ -33,11 +33,13 @@
     int nKeep;
 }
-psCombineParams;
+pmCombineParams;
+
+pmCombineParams *pmCombineParamsAlloc (psStatsOptions statsOptions);
 
 psImage *pmReadoutCombine(psImage *output,
-                          const psList *inputs,
-                          psCombineParams *params,
+                          const psArray *inputs,
                           const psVector *zero,
                           const psVector *scale,
+                          pmCombineParams *params,
                           bool applyZeroScale,
                           float gain,
@@ -72,29 +74,3 @@
 pmFringePoint;
 
-
-/**
- * 
- * The input array fluxLevels consists of Ni vectors, one per mosaic image.
- * Each vector consists of Nj elements, each a measurement of the input
- * flat-field image flux levels. All of these vectors must be constructed with
- * the same number of elements, or the function will return an error. If a chip
- * is missing from a particular image, that element should be set to NaN. The
- * vector chipGains supplies initial guesses for the chip gains. If the vector
- * contains the values 0.0 or NaN for any of the elements, the gain is set to the
- * mean of the valid values. If the vector length does not match the number of
- * chips, an warning is raised, all chip gain guesses will be set to 1.0, and the
- * vector length modified to match the number of chips defined by the supplied
- * fluxLevels. The sourceFlux input vector must be allocated (not NULL), but the
- * routine will set the vector length to the number of source images regardless
- * of the initial state of the vector. All vectors used by this function must be
- * of type PS_DATA_F64.
- * 
- */
-bool pmFlatNormalization(
-    psVector *sourceFlux,
-    psVector *chipGains,
-    psArray *fluxLevels
-);
-
-
 #endif
Index: trunk/psModules/src/imsubtract/Makefile.am
===================================================================
--- trunk/psModules/src/imsubtract/Makefile.am	(revision 6529)
+++ trunk/psModules/src/imsubtract/Makefile.am	(revision 6872)
@@ -4,10 +4,10 @@
 libpsmoduleimsubtract_la_LDFLAGS  = -release $(PACKAGE_VERSION)
 libpsmoduleimsubtract_la_SOURCES  = pmImageSubtract.c \
-    pmSubtractBias.c \
-    pmSubtractSky.c
+    pmSubtractBias.c
+#    pmSubtractSky.c
 
 psmoduleincludedir = $(includedir)
 psmoduleinclude_HEADERS = \
     pmImageSubtract.h \
-    pmSubtractBias.h \
-    pmSubtractSky.h
+    pmSubtractBias.h
+#    pmSubtractSky.h
Index: trunk/psModules/src/imsubtract/pmSubtractBias.h
===================================================================
--- trunk/psModules/src/imsubtract/pmSubtractBias.h	(revision 6529)
+++ trunk/psModules/src/imsubtract/pmSubtractBias.h	(revision 6872)
@@ -1,2 +1,7 @@
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// XXX WARNING: I have completely replaced this file with an OLD VERSION (that works) instead of the
+// one that was being worked on.
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
 /** @file  pmSubtractBias.h
  *
@@ -6,6 +11,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-11-23 23:54:30 $
+ *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -23,8 +28,10 @@
 #include<math.h>
 #include "pslib.h"
-#include "pmAstrometry.h"
+
+#include "pmFPA.h"
 
 typedef enum {
     PM_OVERSCAN_NONE,                         ///< No overscan subtraction
+    PM_OVERSCAN_EDGE,                         ///< Subtract the statistic of pixels along the to-be-determined readout direction
     PM_OVERSCAN_ROWS,                         ///< Subtract rows
     PM_OVERSCAN_COLUMNS,                      ///< Subtract columns
@@ -33,30 +40,38 @@
 
 typedef enum {
-    PM_FIT_NONE,                              ///< No fit
-    PM_FIT_POLYNOMIAL,                        ///< Fit polynomial
-    PM_FIT_SPLINE                             ///< Fit cubic splines
+    PM_FIT_NONE,                        ///< No fit
+    PM_FIT_POLY_ORD,                    ///< Fit ordinary polynomial
+    PM_FIT_POLY_CHEBY,                  ///< Fit Chebyshev 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.
-    pmFit fit,                          ///< PM_FIT_SPLINE, PM_FIT_POLYNOMIAL, or PM_FIT_NONE
-    bool overscan,
-    psStats *stat,                      ///< The statistic to be used in combining overscan data
-    int nBin,                           ///< The amount of binning to be done image pixels.
-    const pmReadout *bias,              ///< A possibly NULL bias pmReadout which is to be subtracted
-    const pmReadout *dark               ///< A possibly NULL bias pmReadout which is to be subtracted
-);
+typedef struct
+{
+    // Inputs
+    bool single;                // Reduce all overscan regions to a single value?
+    pmFit fitType;              // Type of fit to overscan
+    unsigned int order;         // Order of polynomial, or number of spline pieces
+    psStats *stat;              // Statistic to use when reducing the minor direction
+    // Outputs
+    psPolynomial1D *poly;       // Result of polynomial fit
+    psSpline1D *spline;         // Result of spline fit
+}
+pmOverscanOptions;
 
-/******************************************************************************
-DetermineTrimmedImageg() This private routine determines the region of the
-input pmReadout which will be operated on by the various detrend modules.  It
-does a metadata fetch on "CELL.TRIMSEC" for the parent cell of the pmReadout.
- 
-XXX: Consider making a pmUtils.c file and put this routine there.
- *****************************************************************************/
-psImage *p_psDetermineTrimmedImage(
-    pmReadout *in
-);
+pmOverscanOptions *pmOverscanOptionsAlloc(bool single, pmFit fitType, unsigned int order, psStats *stat);
+
+pmReadout *pmSubtractBias(pmReadout *in, pmOverscanOptions *overscanOpts,
+                          const pmReadout *bias, const pmReadout *dark);
+
+#if 0
+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
 
 #endif
Index: trunk/psModules/src/imsubtract/pmSubtractSky.h
===================================================================
--- trunk/psModules/src/imsubtract/pmSubtractSky.h	(revision 6529)
+++ trunk/psModules/src/imsubtract/pmSubtractSky.h	(revision 6872)
@@ -6,6 +6,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-09-28 20:43:52 $
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -23,5 +23,5 @@
 #include<math.h>
 #include "pslib.h"
-#include "pmAstrometry.h"
+#include "pmFPA.h"
 
 // XXX: this is pmFit in pmSubtractBias.c, named psFit here.
Index: trunk/psModules/src/objects/Makefile.am
===================================================================
--- trunk/psModules/src/objects/Makefile.am	(revision 6529)
+++ trunk/psModules/src/objects/Makefile.am	(revision 6872)
@@ -1,12 +1,26 @@
 noinst_LTLIBRARIES = libpsmoduleobjects.la
 
-libpsmoduleobjects_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
+libpsmoduleobjects_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS) -I../pslib/
 libpsmoduleobjects_la_LDFLAGS  = -release $(PACKAGE_VERSION)
 libpsmoduleobjects_la_SOURCES  = \
-    pmObjects.c \
-    pmPSF.c \
-    pmPSFtry.c \
-    pmModelGroup.c \
-    psEllipse.c
+     pmPeaks.c \
+     pmMoments.c \
+     pmModel.c \
+     pmModelGroup.c \
+     pmSource.c \
+     pmSourceSky.c \
+     pmSourceContour.c \
+     pmSourceFitModel.c \
+     pmSourcePhotometry.c \
+     pmSourceIO.c \
+     pmSourceIO_CMF.c \
+     pmSourceIO_CMP.c \
+     pmSourceIO_OBJ.c \
+     pmSourceIO_SX.c \
+     pmSourceIO_RAW.c \
+     pmPSF.c \
+     pmPSF_IO.c \
+     pmPSFtry.c \
+     pmGrowthCurve.c
 
 EXTRA_DIST = \
@@ -18,7 +32,16 @@
 psmoduleincludedir = $(includedir)
 psmoduleinclude_HEADERS = \
-    pmObjects.h \
-    pmPSF.h \
-    pmPSFtry.h \
-    pmModelGroup.h \
-    psEllipse.h
+     pmPeaks.h \
+     pmMoments.h \
+     pmModel.h \
+     pmModelGroup.h \
+     pmSource.h \
+     pmSourceSky.h \
+     pmSourceContour.h \
+     pmSourceFitModel.h \
+     pmSourcePhotometry.h \
+     pmSourceIO.h \
+     pmPSF.h \
+     pmPSF_IO.h \
+     pmPSFtry.h \
+     pmGrowthCurve.h
Index: trunk/psModules/src/objects/models/pmModel_PGAUSS.c
===================================================================
--- trunk/psModules/src/objects/models/pmModel_PGAUSS.c	(revision 6529)
+++ trunk/psModules/src/objects/models/pmModel_PGAUSS.c	(revision 6872)
@@ -26,13 +26,13 @@
 
     if (deriv != NULL) {
-        // note difference from a pure gaussian: q = PAR[1]*r
+        psF32 *dPAR = deriv->data.F32;
         psF32 q = PAR[1]*r*r*t;
-        deriv->data.F32[0] = +1.0;
-        deriv->data.F32[1] = +r;
-        deriv->data.F32[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
-        deriv->data.F32[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
-        deriv->data.F32[4] = -2.0*q*px*X;
-        deriv->data.F32[5] = -2.0*q*py*Y;
-        deriv->data.F32[6] = -q*X*Y;
+        dPAR[0] = +1.0;
+        dPAR[1] = +r;
+        dPAR[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
+        dPAR[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
+        dPAR[4] = -2.0*q*px*X;
+        dPAR[5] = -2.0*q*py*Y;
+        dPAR[6] = -q*X*Y;
     }
     return(f);
@@ -50,5 +50,5 @@
 
     beta_lim[0][0].data.F32[0] = 1000;
-    beta_lim[0][0].data.F32[1] = 10000;
+    beta_lim[0][0].data.F32[1] = 3e6;
     beta_lim[0][0].data.F32[2] = 5;
     beta_lim[0][0].data.F32[3] = 5;
@@ -66,5 +66,5 @@
 
     params_max[0][0].data.F32[0] = 1e5;
-    params_max[0][0].data.F32[1] = 1e6;
+    params_max[0][0].data.F32[1] = 1e8;
     params_max[0][0].data.F32[2] = 1e4;  // this should be set by image dimensions!
     params_max[0][0].data.F32[3] = 1e4;  // this should be set by image dimensions!
@@ -133,4 +133,5 @@
     params[5] = 1.2 / moments->Sy;
     params[6] = 0.0;
+
     return(true);
 }
Index: trunk/psModules/src/objects/models/pmModel_QGAUSS.c
===================================================================
--- trunk/psModules/src/objects/models/pmModel_QGAUSS.c	(revision 6529)
+++ trunk/psModules/src/objects/models/pmModel_QGAUSS.c	(revision 6872)
@@ -26,21 +26,27 @@
     psF32 py = PAR[5]*Y;
     psF32 z  = 0.5*PS_SQR(px) + 0.5*PS_SQR(py) + PAR[6]*X*Y;
-
-    psF32 r  = 1.0 / (1 + PAR[7]*z + pow(z, 2.25));
-    psF32 f  = PAR[1]*r + PAR[0];
+    psF32 zp = pow(z,1.25);
+
+    psF32 r  = 1.0 / (1 + PAR[7]*z + z*zp);
+    // test: psF32 r  = 1.0 / (1 + PAR[7]*z + PS_SQR(z));
+    psF32 r1 = PAR[1]*r;
+    psF32 f  = r1 + PAR[0];
 
     if (deriv != NULL) {
+        psF32 *dPAR = deriv->data.F32;
+
         // note difference from a pure gaussian: q = params->data.F32[1]*r
-        psF32 t = PAR[1]*r*r;
-        psF32 q = t*(PAR[7] + 2.25*pow(z, 1.25));
-
-        deriv->data.F32[0] = +1.0;
-        deriv->data.F32[1] = +r;
-        deriv->data.F32[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
-        deriv->data.F32[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
-        deriv->data.F32[4] = -2.0*q*px*X;
-        deriv->data.F32[5] = -2.0*q*py*Y;
-        deriv->data.F32[6] = -q*X*Y;
-        deriv->data.F32[7] = -t*z;
+        psF32 t = r1*r;
+        psF32 q = t*(PAR[7] + 2.25*zp);
+        // test: psF32 q = t*(PAR[7] + 2*z);
+
+        dPAR[0] = +1.0;
+        dPAR[1] = +r;
+        dPAR[2] = q*(2.0*px*PAR[4] + PAR[6]*Y);
+        dPAR[3] = q*(2.0*py*PAR[5] + PAR[6]*X);
+        dPAR[4] = -2.0*q*px*X;
+        dPAR[5] = -2.0*q*py*Y;
+        dPAR[6] = -q*X*Y;
+        dPAR[7] = -t*z;
     }
     return(f);
@@ -58,5 +64,5 @@
 
     beta_lim[0][0].data.F32[0] = 1000;
-    beta_lim[0][0].data.F32[1] = 10000;
+    beta_lim[0][0].data.F32[1] = 3e6;
     beta_lim[0][0].data.F32[2] = 5;
     beta_lim[0][0].data.F32[3] = 5;
@@ -76,5 +82,5 @@
 
     params_max[0][0].data.F32[0] = 1e5;
-    params_max[0][0].data.F32[1] = 1e6;
+    params_max[0][0].data.F32[1] = 1e8;
     params_max[0][0].data.F32[2] = 1e4;  // this should be set by image dimensions!
     params_max[0][0].data.F32[3] = 1e4;  // this should be set by image dimensions!
@@ -102,4 +108,5 @@
     params[6] = 0.0;
     params[7] = 1.0;
+
     return(true);
 }
@@ -119,9 +126,10 @@
     // the area needs to be multiplied by the integral of f(z)
     norm = 0.0;
-    for (z = 0.005; z < 50; z += 0.01) {
+    for (z = 0.05; z < 50; z += 0.1) {
         f = 1.0 / (1 + PAR[7]*z + pow(z, 2.25));
+        // test: f = 1.0 / (1 + PAR[7]*z + PS_SQR(z));
         norm += f;
     }
-    norm *= 0.01;
+    norm *= 0.1;
 
     psF64 Flux = PAR[1] * Area * norm;
@@ -150,6 +158,7 @@
 
     // we can do this much better with intelligent choices here
-    for (z = 0.0; z < 20.0; z += dz) {
+    for (z = 0.0; z < 30.0; z += dz) {
         f = 1.0 / (1 + PAR[7]*z + pow(z, 2.25));
+        // test: f = 1.0 / (1 + PAR[7]*z + PS_SQR(z));
         if (f < limit)
             break;
@@ -201,6 +210,6 @@
     status &= ((dPAR[1]/PAR[1]) < 0.5);
 
-    if (status)
-        return true;
-    return false;
-}
+    if (!status)
+        return false;
+    return true;
+}
Index: trunk/psModules/src/objects/models/pmModel_RGAUSS.c
===================================================================
--- trunk/psModules/src/objects/models/pmModel_RGAUSS.c	(revision 6529)
+++ trunk/psModules/src/objects/models/pmModel_RGAUSS.c	(revision 6872)
@@ -117,7 +117,7 @@
     psVector *params = model->params;
 
-    EllipseAxes axes;
-    EllipseShape shape;
-    EllipseMoments moments;
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
 
     moments.x2 = PS_SQR(source->moments->Sx);
@@ -125,6 +125,6 @@
     moments.xy = source->moments->Sxy;
 
-    axes = EllipseMomentsToAxes(moments);
-    shape = EllipseAxesToShape(axes);
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
 
     params->data.F32[0] = source->moments->Sky;
Index: trunk/psModules/src/objects/models/pmModel_SGAUSS.c
===================================================================
--- trunk/psModules/src/objects/models/pmModel_SGAUSS.c	(revision 6529)
+++ trunk/psModules/src/objects/models/pmModel_SGAUSS.c	(revision 6872)
@@ -17,5 +17,5 @@
 
 # define SQ(A)((A)*(A))
-psF64 psImageEllipseContour (EllipseAxes axes, double xc, double yc, psImage *image);
+psF64 psImageEllipseContour (psEllipseAxes axes, double xc, double yc, psImage *image);
 psF64 p_psImageGetElementF64(psImage *a, int i, int j);
 
@@ -102,5 +102,5 @@
 
 // measure the flux for the elliptical contour
-psF64 psImageEllipseContour (EllipseAxes axes, double xc, double yc, psImage *image)
+psF64 psImageEllipseContour (psEllipseAxes axes, double xc, double yc, psImage *image)
 {
 
@@ -149,7 +149,7 @@
     psF32     *params   = model->params->data.F32;
 
-    EllipseAxes axes;
-    EllipseShape shape;
-    EllipseMoments moments;
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
 
     moments.x2 = PS_SQR(sMoments->Sx);
@@ -158,6 +158,6 @@
 
     // solve the math to go from Moments To Shape
-    axes = EllipseMomentsToAxes(moments);
-    shape = EllipseAxesToShape(axes);
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
 
     params[0] = sMoments->Sky;
@@ -199,7 +199,7 @@
     float f1, f2;
 
-    EllipseAxes axes;
-    EllipseShape shape;
-    EllipseMoments moments;
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
 
     moments.x2 = PS_SQR(sMoments->Sx);
@@ -208,6 +208,6 @@
 
     // solve the math to go from Moments To Shape
-    axes = EllipseMomentsToAxes(moments);
-    shape = EllipseAxesToShape(axes);
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
 
     params[0] = sMoments->Sky;
@@ -265,9 +265,9 @@
 psF64 pmModelRadius_SGAUSS  (const psVector *params, psF64 flux)
 {
-    psF64 r, z, pr, f;
+    psF64 r, z = 0.0, pr, f;
     psF32 *PAR = params->data.F32;
 
-    EllipseAxes axes;
-    EllipseShape shape;
+    psEllipseAxes axes;
+    psEllipseShape shape;
 
     if (flux <= 0)
@@ -283,5 +283,5 @@
     shape.sxy = PAR[6];
 
-    axes = EllipseShapeToAxes (shape);
+    axes = psEllipseShapeToAxes (shape);
     psF64 dr = 1.0 / axes.major;
     psF64 limit = flux / PAR[1];
@@ -327,6 +327,6 @@
     psF32 dP;
     bool  status;
-    EllipseAxes axes;
-    EllipseShape shape;
+    psEllipseAxes axes;
+    psEllipseShape shape;
 
     psF32 *PAR  = model->params->data.F32;
@@ -337,5 +337,5 @@
     shape.sxy = PAR[6];
 
-    axes = EllipseShapeToAxes (shape);
+    axes = psEllipseShapeToAxes (shape);
 
     dP = 0;
Index: trunk/psModules/src/objects/models/pmModel_ZGAUSS.c
===================================================================
--- trunk/psModules/src/objects/models/pmModel_ZGAUSS.c	(revision 6529)
+++ trunk/psModules/src/objects/models/pmModel_ZGAUSS.c	(revision 6872)
@@ -85,6 +85,6 @@
     psF32 *PAR = params->data.F32;
 
-    EllipseAxes axes;
-    EllipseShape shape;
+    psEllipseAxes axes;
+    psEllipseShape shape;
 
     if (flux <= 0)
@@ -100,5 +100,5 @@
     shape.sxy = PAR[6];
 
-    axes = EllipseShapeToAxes (shape);
+    axes = psEllipseShapeToAxes (shape);
     psF64 dr = 1.0 / axes.major;
     psF64 limit = flux / PAR[1];
@@ -124,7 +124,7 @@
     psVector *params = model->params;
 
-    EllipseAxes axes;
-    EllipseShape shape;
-    EllipseMoments moments;
+    psEllipseAxes axes;
+    psEllipseShape shape;
+    psEllipseMoments moments;
 
     moments.x2 = PS_SQR(source->moments->Sx);
@@ -132,6 +132,6 @@
     moments.xy = source->moments->Sxy;
 
-    axes = EllipseMomentsToAxes(moments);
-    shape = EllipseAxesToShape(axes);
+    axes = psEllipseMomentsToAxes(moments);
+    shape = psEllipseAxesToShape(axes);
 
     params->data.F32[0] = source->moments->Sky;
Index: trunk/psModules/src/objects/pmGrowthCurve.c
===================================================================
--- trunk/psModules/src/objects/pmGrowthCurve.c	(revision 6872)
+++ trunk/psModules/src/objects/pmGrowthCurve.c	(revision 6872)
@@ -0,0 +1,130 @@
+/** @file  pmGrowthCurve.c
+ *
+ *  Measure the curve-of-growth for sources
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <pslib.h>
+#include "pmGrowthCurve.h"
+
+static void pmGrowthCurveFree (pmGrowthCurve *growth)
+{
+
+    if (growth == NULL)
+        return;
+
+    psFree (growth->radius);
+    psFree (growth->apMag);
+    return;
+}
+
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius)
+{
+
+    pmGrowthCurve *growth = psAlloc (sizeof(pmGrowthCurve));
+    psMemSetDeallocator(growth, (psFreeFunc) pmGrowthCurveFree);
+
+    // fractional pixel radii are not well defined; use integer steps
+    growth->radius = psVectorCreate (NULL, minRadius, maxRadius, 1.0, PS_TYPE_F32);
+    growth->apMag  = psVectorAlloc (growth->radius->n, PS_TYPE_F32);
+
+    // XXX may want to extend this to allow for a different refRadius;
+    growth->refRadius = refRadius;
+    growth->maxRadius = maxRadius;
+    growth->apLoss = 0.0;
+    growth->fitMag = 0.0;
+    return growth;
+}
+
+psF32 pmGrowthCurveCorrect (pmGrowthCurve *growth, psF32 radius)
+{
+
+    float apRad = psVectorInterpolate (growth->radius, growth->apMag, radius);
+    float apCor = growth->apRef - apRad;
+    return apCor;
+}
+
+// return the last entry below or first entry above key value
+int psVectorBracket (psVector *index, psF32 key, bool above)
+{
+
+    int N;
+    int Nlo = 0;
+    int Nhi = index->n;
+
+    if (above) {
+        while (Nhi - Nlo > 10) {
+            N = 0.5*(Nlo + Nhi);
+            if (index->data.F32[N] > key) {
+                Nhi = N;
+            } else {
+                Nlo = N - 1;
+            }
+        }
+        // at this point, index[Nhi] > key >= index[Nlo]
+        N = Nlo;
+        while ((index->data.F32[N] <= key) && (N < Nhi)) {
+            N++;
+        }
+        return (N);
+    }
+    while (Nhi - Nlo > 10) {
+        N = 0.5*(Nlo + Nhi);
+        if (index->data.F32[N] < key) {
+            Nlo = N;
+        } else {
+            Nhi = N + 1;
+        }
+    }
+    // at this point, index[Nhi] >= key > index[Nlo]
+    N = Nhi;
+    while ((index->data.F32[N] >= key) && (N > Nlo)) {
+        N--;
+    }
+    return (N);
+}
+
+// search for the bins bounding key in index, interpolate the corresponding values
+psF32 psVectorInterpolate (psVector *index, psVector *value, psF32 key)
+{
+
+    int n0 = 0;
+    int n1 = 0;
+
+    // extrapolate at ends
+    if (key < index->data.F32[0]) {
+        n0 = 0;
+        n1 = 1;
+    }
+
+    // extrapolate at ends
+    if (key > index->data.F32[index->n-1]) {
+        n0 = index->n-2;
+        n1 = index->n-1;
+    }
+
+    if (n1 == 0) {
+        n0 = psVectorBracket (index, key, FALSE);
+        n1 = n0 + 1;
+    }
+
+    if (n0 == index->n-1) {
+        n1 = n0;
+        n0 = n1 - 1;
+    }
+
+    float dy = value->data.F32[n1] - value->data.F32[n0];
+    float dx = index->data.F32[n1] - index->data.F32[n0];
+    float dX = key - index->data.F32[n0];
+    float dY = dX * (dy/dx);
+    float result = value->data.F32[n0] + dY;
+    return result;
+}
+
Index: trunk/psModules/src/objects/pmGrowthCurve.h
===================================================================
--- trunk/psModules/src/objects/pmGrowthCurve.h	(revision 6872)
+++ trunk/psModules/src/objects/pmGrowthCurve.h	(revision 6872)
@@ -0,0 +1,24 @@
+
+# ifndef PM_GROWTH_CURVE_H
+# define PM_GROWTH_CURVE_H
+
+typedef struct
+{
+    psVector *radius;
+    psVector *apMag;
+    psF32 refRadius;
+    psF32 maxRadius;
+    psF32 fitMag;
+    psF32 apRef;   // apMag[refRadius]
+    psF32 apLoss;  // fitMag - apRef
+}
+pmGrowthCurve;
+
+pmGrowthCurve *pmGrowthCurveAlloc (psF32 minRadius, psF32 maxRadius, psF32 refRadius);
+int psVectorBracket (psVector *index, psF32 key, bool above);
+psF32 psVectorInterpolate (psVector *index, psVector *value, psF32 key);
+psF32 pmGrowthCurveCorrect (pmGrowthCurve *growth, psF32 radius);
+
+// XXX psVectorBracket,Interpolate should be put in pslib
+
+# endif /* PM_GROWTH_CURVE_H */
Index: trunk/psModules/src/objects/pmModel.c
===================================================================
--- trunk/psModules/src/objects/pmModel.c	(revision 6872)
+++ trunk/psModules/src/objects/pmModel.c	(revision 6872)
@@ -0,0 +1,225 @@
+/** @file  pmModel.c
+ *
+ *  Functions to define and manipulate object models
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmModel.h"
+
+/* The following types and functions need to be known by pmModel.c and are defined in
+   pmModelGroup.h.  The use of pmSource and other types in pmModelGroup.h prevent us from
+   directly including it here  ***/
+
+typedef psMinimizeLMChi2Func pmModelFunc;
+typedef bool (*pmModelFitStatusFunc)(pmModel *model);
+pmModelFunc pmModelFunc_GetFunction (pmModelType type);
+pmModelFitStatusFunc pmModelFitStatusFunc_GetFunction (pmModelType type);
+int pmModelParameterCount(pmModelType type);
+
+static void modelFree(pmModel *tmp)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    psFree(tmp->params);
+    psFree(tmp->dparams);
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+}
+
+/******************************************************************************
+pmModelAlloc(): Allocate the pmModel structure, along with its parameters,
+and initialize the type member.  Initialize the params to 0.0.
+XXX EAM: simplifying code with pmModelParameterCount
+*****************************************************************************/
+pmModel *pmModelAlloc(pmModelType type)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmModel *tmp = (pmModel *) psAlloc(sizeof(pmModel));
+
+    tmp->type = type;
+    tmp->chisq = 0.0;
+    tmp->nIter = 0;
+    tmp->radiusTMP = 0;
+    tmp->status = PM_MODEL_UNTRIED;
+
+    psS32 Nparams = pmModelParameterCount(type);
+    if (Nparams == 0) {
+        psError(PS_ERR_UNKNOWN, true, "Undefined pmModelType");
+        return(NULL);
+    }
+
+    tmp->params  = psVectorAlloc(Nparams, PS_TYPE_F32);
+    tmp->dparams = psVectorAlloc(Nparams, PS_TYPE_F32);
+
+    for (psS32 i = 0; i < tmp->params->n; i++) {
+        tmp->params->data.F32[i] = 0.0;
+        tmp->dparams->data.F32[i] = 0.0;
+    }
+
+    psMemSetDeallocator(tmp, (psFreeFunc) modelFree);
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model)
+{
+
+    pmModel *new = pmModelAlloc (model->type);
+
+    new->chisq  = model->chisq;
+    new->nDOF   = model->nDOF;
+    new->nIter  = model->nIter;
+    new->status = model->status;
+    new->radiusTMP = model->radiusTMP;
+
+    for (int i = 0; i < new->params->n; i++) {
+        new->params->data.F32[i]  = model->params->data.F32[i];
+        new->dparams->data.F32[i] = model->dparams->data.F32[i];
+    }
+
+    return (new);
+}
+
+/******************************************************************************
+    pmModelEval(source, level, row): evaluates the model function at the specified coords.  
+     
+    NOTE: The coords are in subImage source->pixel coords, not image coords.
+     
+    XXX: Use static vectors for x (NO: needs to be thread safe)
+*****************************************************************************/
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_PTR_NON_NULL(model->params, false);
+
+    // Allocate the x coordinate structure and convert row/col to image space.
+    //
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    x->data.F32[0] = (psF32) (col + image->col0);
+    x->data.F32[1] = (psF32) (row + image->row0);
+    psF32 tmpF;
+    pmModelFunc modelFunc;
+
+    modelFunc = pmModelFunc_GetFunction (model->type);
+    tmpF = modelFunc (NULL, model->params, x);
+    psFree(x);
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmpF);
+}
+
+bool AddOrSubModel(psImage *image,
+                   psImage *mask,
+                   pmModel *model,
+                   bool center,
+                   bool sky,
+                   bool add
+                      )
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+
+    PS_ASSERT_PTR_NON_NULL(model, false);
+    PS_ASSERT_IMAGE_NON_NULL(image, false);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, false);
+
+    psVector *x = psVectorAlloc(2, PS_TYPE_F32);
+    psVector *params = model->params;
+    pmModelFunc modelFunc = pmModelFunc_GetFunction (model->type);
+    psS32 imageCol;
+    psS32 imageRow;
+    psF32 skyValue = params->data.F32[0];
+    psF32 pixelValue;
+
+    for (psS32 i = 0; i < image->numRows; i++) {
+        for (psS32 j = 0; j < image->numCols; j++) {
+            if ((mask != NULL) && mask->data.U8[i][j])
+                continue;
+
+            // Convert i/j to image coord space:
+            // 'center' option changes meaning of i,j:
+            if (center) {
+                imageCol = j - 0.5*image->numCols + model->params->data.F32[2];
+                imageRow = i - 0.5*image->numRows + model->params->data.F32[3];
+            } else {
+                imageCol = j + image->col0;
+                imageRow = i + image->row0;
+            }
+
+            x->data.F32[0] = (float) imageCol;
+            x->data.F32[1] = (float) imageRow;
+
+            // set the appropriate pixel value for this coordinate
+            if (sky) {
+                pixelValue = modelFunc (NULL, params, x);
+            } else {
+                pixelValue = modelFunc (NULL, params, x) - skyValue;
+            }
+
+
+            // add or subtract the value
+            if (add
+               ) {
+                image->data.F32[i][j] += pixelValue;
+            }
+            else {
+                image->data.F32[i][j] -= pixelValue;
+            }
+        }
+    }
+    psFree(x);
+    psTrace(__func__, 3, "---- %s(true) end ----\n", __func__);
+    return(true);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelAdd(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                bool center,
+                bool sky)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, center, sky, true);
+    psTrace(__func__, 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+ *****************************************************************************/
+bool pmModelSub(psImage *image,
+                psImage *mask,
+                pmModel *model,
+                bool center,
+                bool sky)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    psBool rc = AddOrSubModel(image, mask, model, center, sky, false);
+    psTrace(__func__, 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+// check for success of model fit
+bool pmModelFitStatus (pmModel *model)
+{
+
+    bool status;
+
+    pmModelFitStatusFunc statusFunc = pmModelFitStatusFunc_GetFunction (model->type);
+    status = statusFunc (model);
+
+    return (status);
+}
+
Index: trunk/psModules/src/objects/pmModel.h
===================================================================
--- trunk/psModules/src/objects/pmModel.h	(revision 6872)
+++ trunk/psModules/src/objects/pmModel.h	(revision 6872)
@@ -0,0 +1,113 @@
+/** @file  pmObjects.h
+ *
+ *  Functions to define and manipulate object models
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_MODEL_H
+# define PM_MODEL_H
+
+// type of model carried by the pmModel structure
+typedef int pmModelType;
+
+typedef enum {
+    PM_MODEL_UNTRIED,   ///< model fit not yet attempted
+    PM_MODEL_SUCCESS,   ///< model fit succeeded
+    PM_MODEL_NONCONVERGE,  ///< model fit did not converge
+    PM_MODEL_OFFIMAGE,   ///< model fit drove out of range
+    PM_MODEL_BADARGS   ///< model fit called with invalid args
+} pmModelStatus;
+
+/** pmModel data structure
+ *
+ * Every source may have two types of models: a PSF model and a EXT (extended-source)
+ * model. The PSF model represents the best fit of the image PSF to the specific
+ * object. In this case, the PSF-dependent parameters are specified for the
+ * object by the PSF, not by the fit. The EXT model represents the best fit of
+ * the given model to the object, with all shape parameters floating in the fit.
+ *
+ */
+typedef struct
+{
+    pmModelType type;   ///< Model to be used.
+    psVector *params;   ///< Paramater values.
+    psVector *dparams;   ///< Parameter errors.
+    float chisq;   ///< Fit chi-squared.
+    float chisqNorm;   ///< re-normalized fit chi-squared.
+    int nDOF;    ///< number of degrees of freedom
+    int nIter;    ///< number of iterations to reach min
+    pmModelStatus status;  ///< fit status
+    float radiusTMP;   ///< fit radius actually used
+}
+pmModel;
+
+/** pmModelAlloc()
+ *
+ */
+pmModel *pmModelAlloc(pmModelType type);
+
+// copy model to a new structure
+pmModel *pmModelCopy (pmModel *model);
+
+psF32 pmModelEval(pmModel *model, psImage *image, psS32 col, psS32 row);
+
+/** pmModelAdd()
+ *
+ * Add the given source model flux to/from the provided image. The boolean
+ * option center selects if the source is re-centered to the image center or if
+ * it is placed at its centroid location. The boolean option sky selects if the
+ * background sky is applied (TRUE) or not. The pixel range in the target image
+ * is at most the pixel range specified by the source.pixels image. The success
+ * status is returned.
+ *
+ */
+bool pmModelAdd(
+    psImage *image,   ///< The output image (float)
+    psImage *mask,   ///< The image pixel mask (valid == 0)
+    pmModel *model,   ///< The input pmModel
+    bool center,   ///< A boolean flag that determines whether pixels are centered
+    bool sky    ///< A boolean flag that determines if the sky is subtracted
+);
+
+
+/** pmModelSub()
+ *
+ * Subtract the given source model flux to/from the provided image. The
+ * boolean option center selects if the source is re-centered to the image center
+ * or if it is placed at its centroid location. The boolean option sky selects if
+ * the background sky is applied (TRUE) or not. The pixel range in the target
+ * image is at most the pixel range specified by the source.pixels image. The
+ * success status is returned.
+ *
+ */
+bool pmModelSub(
+    psImage *image,   ///< The output image (float)
+    psImage *mask,   ///< The image pixel mask (valid == 0)
+    pmModel *model,   ///< The input pmModel
+    bool center,   ///< A boolean flag that determines whether pixels are centered
+    bool sky    ///< A boolean flag that determines if the sky is subtracted
+);
+
+/** pmModelFitStatus()
+ *
+ * This function wraps the call to the model-specific function returned by
+ * pmModelFitStatusFunc_GetFunction.  The model-specific function examines the
+ * model parameters, parameter errors, Chisq, S/N, and other parameters available
+ * from model to decide if the particular fit was successful or not.
+ *
+ * XXX: Must code this.
+ *
+ */
+bool pmModelFitStatus(
+    pmModel *model                      ///< Model to be used
+);
+
+# endif /* PM_MODEL_H */
Index: trunk/psModules/src/objects/pmModelGroup.c
===================================================================
--- trunk/psModules/src/objects/pmModelGroup.c	(revision 6529)
+++ trunk/psModules/src/objects/pmModelGroup.c	(revision 6872)
@@ -1,8 +1,35 @@
-# include "pmModelGroup.h"
-
+/** @file  pmModelGroup.c
+ *
+ *  Functions to define and manipulate object model attributes
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+
+// XXX shouldn't these be defined for us in pslib.h ???
 double hypot(double x, double y);
 double sqrt (double x);
 
-#include "psEllipse.h"
 #include "models/pmModel_GAUSS.c"
 #include "models/pmModel_PGAUSS.c"
@@ -25,5 +52,4 @@
     if (modelGroup == NULL)
         return;
-    psFree (modelGroup);
     return;
 }
@@ -60,4 +86,11 @@
     }
     Nmodels = Nnew;
+    return;
+}
+
+void pmModelGroupCleanup (void)
+{
+
+    psFree (models);
     return;
 }
@@ -153,2 +186,28 @@
     return (models[type].name);
 }
+
+/******************************************************************************
+    pmSourceModelGuess(source, model): This function allocates a new
+    pmModel structure based on the given modelType specified in the argument list.  
+    The corresponding pmModelGuess function is returned, and used to 
+    supply the values of the params array in the pmModel structure.  
+     
+    XXX: Many parameters are based on the src->moments structure, which is in
+    image, not subImage coords.  Therefore, the calls to the model evaluation
+    functions will be in image, not subImage coords.  Remember this.
+*****************************************************************************/
+pmModel *pmSourceModelGuess(pmSource *source,
+                            pmModelType modelType)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+
+    pmModel *model = pmModelAlloc(modelType);
+
+    pmModelGuessFunc modelGuessFunc = pmModelGuessFunc_GetFunction(modelType);
+    modelGuessFunc(model, source);
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(model);
+}
+
Index: trunk/psModules/src/objects/pmModelGroup.h
===================================================================
--- trunk/psModules/src/objects/pmModelGroup.h	(revision 6529)
+++ trunk/psModules/src/objects/pmModelGroup.h	(revision 6872)
@@ -9,16 +9,84 @@
  *  @author EAM, IfA
  *
- *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-12-24 01:24:32 $
+ *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
  * 
  */
-#include "pmObjects.h"
-#include "pmPSF.h"
-/**
- * 
- * This function returns the number of parameters used by the listed function.
- * 
+
+# ifndef PM_MODEL_GROUP_H
+# define PM_MODEL_GROUP_H
+
+//  This function is the model chi-square minimization function for this model.
+typedef psMinimizeLMChi2Func pmModelFunc;
+
+// This function returns the integrated flux for the given model parameters.
+typedef psF64 (*pmModelFlux)(const psVector *params);
+
+
+// This function returns the radius at which the given model and parameters
+// achieves the given flux.
+typedef psF64 (*pmModelRadius)(const psVector *params, double flux);
+
+/*  This function sets the model parameter limits vectors for the given model
+ */
+typedef bool (*pmModelLimits)(psVector **beta_lim, psVector **params_min, psVector **params_max);
+
+/*  This function provides the model guess parameters based on the details of
+ *   the given source.
+ */
+typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source);
+
+
+/*  This function constructs the PSF model for the given source based on the
+ *  supplied psf and the EXT model for the object.
+ */
+typedef bool (*pmModelFromPSFFunc)(pmModel *modelPSF, pmModel *modelEXT, pmPSF *psf);
+
+/*  This function returns the success / failure status of the given model fit
+ */
+typedef bool (*pmModelFitStatusFunc)(pmModel *model);
+
+/* Every model instance belongs to a class of models, defined by the value of
+ * the pmModelType type entry. Various functions need access to information about
+ * each of the models. Some of this information varies from model to model, and
+ * may depend on the current parameter values or other data quantities. In order
+ * to keep the code from requiring the information about each model to be coded
+ * into the low-level fitting routines, we define a collection of functions which
+ * allow us to abstract this type of model-dependent information. These generic
+ * functions take the model type and return the corresponding function pointer
+ * for the specified model. Each model is defined by creating this collection of
+ * specific functions, and placing them in a single file for each model. We
+ * define the following structure to carry the collection of information about
+ * the models.
+ */
+typedef struct
+{
+    char *name;
+    int nParams;
+    pmModelFunc          modelFunc;
+    pmModelFlux          modelFlux;
+    pmModelRadius        modelRadius;
+    pmModelLimits        modelLimits;
+    pmModelGuessFunc     modelGuessFunc;
+    pmModelFromPSFFunc   modelFromPSFFunc;
+    pmModelFitStatusFunc modelFitStatusFunc;
+}
+pmModelGroup;
+
+// allocate a pmModelGroup to hold nModels entries
+pmModelGroup *pmModelGroupAlloc (int nModels);
+
+// initialize the internal (static) model group with the default models
+void pmModelGroupInit (void);
+
+// free the internal (static) model group
+void pmModelGroupCleanup (void);
+
+// add a new model to the internal (static) model group
+void pmModelGroupAdd (pmModelGroup *model);
+
+/* This function returns the number of parameters used by the listed function.
  */
 int pmModelParameterCount(
@@ -27,8 +95,5 @@
 
 
-/**
- * 
- * This function returns the user-space model names for the specified model type.
- * 
+/* This function returns the user-space model names for the specified model type.
  */
 char *pmModelGetType(
@@ -46,62 +111,4 @@
 );
 
-
-#ifndef PM_MODEL_GROUP_H
-#define PM_MODEL_GROUP_H
-
-/**
- * 
- *  This function is the model chi-square minimization function for this model.
- * 
- */
-typedef psMinimizeLMChi2Func pmModelFunc;
-
-
-/**
- * 
- * This function returns the integrated flux for the given model parameters.
- */
-typedef psF64 (*pmModelFlux)(const psVector *params);
-
-
-/**
- * 
- *  This function returns the radius at which the given model and parameters
- *  achieves the given flux.
- * 
- */
-typedef psF64 (*pmModelRadius)(const psVector *params, double flux);
-
-/**
- * 
- *  This function sets the model parameter limits vectors for the given model
- * 
- */
-typedef bool (*pmModelLimits)(psVector **beta_lim, psVector **params_min, psVector **params_max);
-
-/**
- * 
- *  This function provides the model guess parameters based on the details of
- *   the given source.
- * 
- */
-typedef bool (*pmModelGuessFunc)(pmModel *model, pmSource *source);
-
-
-/**
- * 
- *  This function constructs the PSF model for the given source based on the
- *  supplied psf and the FLT model for the object.
- * 
- */
-typedef bool (*pmModelFromPSFFunc)(pmModel *modelPSF, pmModel *modelFLT, pmPSF *psf);
-
-/**
- * 
- *  This function returns the success / failure status of the given model fit
- * 
- */
-typedef bool (*pmModelFitStatusFunc)(pmModel *model);
-
 /**
  * 
@@ -110,5 +117,4 @@
  * 
  */
-
 
 /**
@@ -177,44 +183,17 @@
 
 
+/** pmSourceModelGuess()
+ *
+ * Convert available data to an initial guess for the given model. This
+ * function allocates a pmModel entry for the pmSource structure based on the
+ * provided model selection. The method of defining the model parameter guesses
+ * are specified for each model below. The guess values are placed in the model
+ * parameters. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+pmModel *pmSourceModelGuess(
+    pmSource *source,   ///< The input pmSource
+    pmModelType model   ///< The type of model to be created.
+);
 
-
-/**
- * 
- * Every model instance belongs to a class of models, defined by the value of
- * the pmModelType type entry. Various functions need access to information about
- * each of the models. Some of this information varies from model to model, and
- * may depend on the current parameter values or other data quantities. In order
- * to keep the code from requiring the information about each model to be coded
- * into the low-level fitting routines, we define a collection of functions which
- * allow us to abstract this type of model-dependent information. These generic
- * functions take the model type and return the corresponding function pointer
- * for the specified model. Each model is defined by creating this collection of
- * specific functions, and placing them in a single file for each model. We
- * define the following structure to carry the collection of information about
- * the models.
- * 
- */
-typedef struct
-{
-    char *name;
-    int nParams;
-    pmModelFunc          modelFunc;
-    pmModelFlux          modelFlux;
-    pmModelRadius        modelRadius;
-    pmModelLimits        modelLimits;
-    pmModelGuessFunc     modelGuessFunc;
-    pmModelFromPSFFunc   modelFromPSFFunc;
-    pmModelFitStatusFunc modelFitStatusFunc;
-}
-pmModelGroup;
-
-// allocate a pmModelGroup to hold nModels entries
-pmModelGroup *pmModelGroupAlloc (int nModels);
-
-// initialize the internal (static) model group with the default models
-void pmModelGroupInit (void);
-
-// add a new model to the internal (static) model group
-void pmModelGroupAdd (pmModelGroup *model);
-
-# endif
+# endif /* PM_MODEL_GROUP_H */
Index: trunk/psModules/src/objects/pmMoments.c
===================================================================
--- trunk/psModules/src/objects/pmMoments.c	(revision 6872)
+++ trunk/psModules/src/objects/pmMoments.c	(revision 6872)
@@ -0,0 +1,39 @@
+/** @file  pmMoments.c
+ *
+ *  Functions defining the pmMoments structure
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include "pslib.h"
+#include "pmMoments.h"
+
+/******************************************************************************
+pmMomentsAlloc(): Allocate the pmMoments structure and initialize the members
+to zero.
+*****************************************************************************/
+pmMoments *pmMomentsAlloc()
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmMoments *tmp = (pmMoments *) psAlloc(sizeof(pmMoments));
+    tmp->x = 0.0;
+    tmp->y = 0.0;
+    tmp->Sx = 0.0;
+    tmp->Sy = 0.0;
+    tmp->Sxy = 0.0;
+    tmp->Sum = 0.0;
+    tmp->Peak = 0.0;
+    tmp->Sky = 0.0;
+    tmp->nPixels = 0;
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
Index: trunk/psModules/src/objects/pmMoments.h
===================================================================
--- trunk/psModules/src/objects/pmMoments.h	(revision 6872)
+++ trunk/psModules/src/objects/pmMoments.h	(revision 6872)
@@ -0,0 +1,45 @@
+/** @file  pmMoments.h
+ *
+ *  Definitions of the moments structure
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_MOMENTS_H
+# define PM_MOMENTS_H
+
+/** pmMoments data structure
+ *
+ * One of the simplest measurements which can be made quickly for an object
+ * are the object moments. We specify a structure to carry the moment information
+ * for a specific source:
+ *
+ */
+typedef struct
+{
+    float x;     ///< X-coord of centroid.
+    float y;     ///< Y-coord of centroid.
+    float Sx;    ///< x-second moment.
+    float Sy;    ///< y-second moment.
+    float Sxy;   ///< xy cross moment.
+    float Sum;   ///< Pixel sum above sky (background).
+    float Peak;  ///< Peak counts above sky.
+    float Sky;   ///< Sky level (background).
+    float dSky;  ///< local Sky variance
+    float SN;    ///< approx signal-to-noise
+    int nPixels; ///< Number of pixels used.
+}
+pmMoments;
+
+/** pmMomentsAlloc()
+ *
+ */
+pmMoments *pmMomentsAlloc();
+
+# endif
Index: trunk/psModules/src/objects/pmObjects.h
===================================================================
--- trunk/psModules/src/objects/pmObjects.h	(revision 6529)
+++ trunk/psModules/src/objects/pmObjects.h	(revision 6872)
@@ -10,6 +10,6 @@
  *  @author GLG, MHPCC
  *
- *  @version $Revision: 1.5 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-12-24 01:24:32 $
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -27,535 +27,4 @@
 #include <math.h>
 #include "pslib.h"
-#include "pmAstrometry.h"
-/**
- * In the object analysis process, we will use specific mask values to mark the
- * image pixels. The following structure defines the relevant mask values.
- *
- * XXX: This is probably a bad solution: we will want to set mask values
- * outside of the PSPHOT code.  Perhaps we can set up a registered set of mask
- * values with specific meanings that other functions can add to or define?
- */
-typedef enum {
-    PSPHOT_MASK_CLEAR     = 0x00,
-    PSPHOT_MASK_INVALID   = 0x01,
-    PSPHOT_MASK_SATURATED = 0x02,
-    PSPHOT_MASK_MARKED    = 0x08,
-} psphotMaskValues;
-
-
-/** pmPeakType
- *
- *  A peak pixel may have several features which may be determined when the
- *  peak is found or measured. These are specified by the pmPeakType enum.
- *  PM_PEAK_LONE represents a single pixel which is higher than its 8 immediate
- *  neighbors.  The PM_PEAK_EDGE represents a peak pixel which touching the image
- *  edge. The PM_PEAK_FLAT represents a peak pixel which has more than a specific
- *  number of neighbors at the same value, within some tolarence:
- *
- */
-typedef enum {
-    PM_PEAK_LONE,                       ///< Isolated peak.
-    PM_PEAK_EDGE,                       ///< Peak on edge.
-    PM_PEAK_FLAT,                       ///< Peak has equal-value neighbors.
-    PM_PEAK_UNDEF                       ///< Undefined.
-} pmPeakType;
-
-
-/** pmPeak data structure
- *
- *  A source has the capacity for several types of measurements. The
- *  simplest measurement of a source is the location and flux of the peak pixel
- *  associated with the source:
- *
- */
-typedef struct
-{
-    int x;                              ///< X-coordinate of peak pixel.
-    int y;                              ///< Y-coordinate of peak pixel.
-    float counts;                       ///< Value of peak pixel (above sky?).
-    pmPeakType class;                   ///< Description of peak.
-}
-pmPeak;
-
-
-/** pmMoments data structure
- *
- * One of the simplest measurements which can be made quickly for an object
- * are the object moments. We specify a structure to carry the moment information
- * for a specific source:
- *
- */
-typedef struct
-{
-    float x;    ///< X-coord of centroid.
-    float y;    ///< Y-coord of centroid.
-    float Sx;    ///< x-second moment.
-    float Sy;    ///< y-second moment.
-    float Sxy;    ///< xy cross moment.
-    float Sum;    ///< Pixel sum above sky (background).
-    float Peak;    ///< Peak counts above sky.
-    float Sky;    ///< Sky level (background).
-    float SN;    ///< approx signal-to-noise
-    int nPixels;   ///< Number of pixels used.
-}
-pmMoments;
-
-
-/** pmPSFClump data structure
- *
- * A collection of object moment measurements can be used to determine
- * approximate object classes. The key to this analysis is the location and
- * statistics (in the second-moment plane,
- *
- */
-typedef struct
-{
-    float X;
-    float dX;
-    float Y;
-    float dY;
-}
-pmPSFClump;
-
-// type of model carried by the pmModel structure
-typedef int pmModelType;
-
-typedef enum {
-    PM_MODEL_UNTRIED,               ///< model fit not yet attempted
-    PM_MODEL_SUCCESS,               ///< model fit succeeded
-    PM_MODEL_NONCONVERGE,           ///< model fit did not converge
-    PM_MODEL_OFFIMAGE,              ///< model fit drove out of range
-    PM_MODEL_BADARGS                ///< model fit called with invalid args
-} pmModelStatus;
-
-/** pmModel data structure
- *
- * Every source may have two types of models: a PSF model and a FLT (floating)
- * model. The PSF model represents the best fit of the image PSF to the specific
- * object. In this case, the PSF-dependent parameters are specified for the
- * object by the PSF, not by the fit. The FLT model represents the best fit of
- * the given model to the object, with all parameters floating in the fit.
- *
- */
-typedef struct
-{
-    pmModelType type;   ///< Model to be used.
-    psVector *params;   ///< Paramater values.
-    psVector *dparams;   ///< Parameter errors.
-    float chisq;   ///< Fit chi-squared.
-    int nDOF;    ///< number of degrees of freedom
-    int nIter;    ///< number of iterations to reach min
-    int status;         ///< fit status
-    float radius;   ///< fit radius actually used
-}
-pmModel;
-
-/** pmSourceType enumeration
- *
- * A given source may be identified as most-likely to be one of several source
- * types. The pmSource entry pmSourceType defines the current best-guess for this
- * source.
- *
- * XXX: The values given below are currently illustrative and will require
- * some modification as the source classification code is developed. (TBD)
- *
- */
-typedef enum {
-    PM_SOURCE_DEFECT,                   ///< a cosmic-ray
-    PM_SOURCE_SATURATED,                ///< random saturated pixels
-
-    PM_SOURCE_SATSTAR,                  ///< a saturated star
-    PM_SOURCE_PSFSTAR,                  ///< a PSF star
-    PM_SOURCE_GOODSTAR,                 ///< a good-quality star
-
-    PM_SOURCE_POOR_FIT_PSF,             ///< poor quality PSF fit
-    PM_SOURCE_FAIL_FIT_PSF,             ///< failed to get a good PSF fit
-    PM_SOURCE_FAINTSTAR,                ///< below S/N cutoff
-
-    PM_SOURCE_GALAXY,                   ///< an extended object (galaxy)
-    PM_SOURCE_FAINT_GALAXY,             ///< a galaxy below S/N cutoff
-    PM_SOURCE_DROP_GALAXY,              ///< ?
-    PM_SOURCE_FAIL_FIT_GAL,             ///< failed on the galaxy fit
-    PM_SOURCE_POOR_FIT_GAL,             ///< poor quality galaxy fit
-
-    PM_SOURCE_OTHER,                    ///< unidentified
-} pmSourceType;
-
-/** pmSource data structure
- *
- *  This source has the capacity for several types of measurements. The
- *  simplest measurement of a source is the location and flux of the peak pixel
- *  associated with the source:
- *
- */
-typedef struct
-{
-    pmPeak *peak;   ///< Description of peak pixel.
-    psImage *pixels;   ///< Rectangular region including object pixels.
-    psImage *weight;   ///< Image variance.
-    psImage *mask;   ///< Mask which marks pixels associated with objects.
-    pmMoments *moments;   ///< Basic moments measure for the object.
-    pmModel *modelPSF;   ///< PSF Model fit (parameters and type)
-    pmModel *modelFLT;   ///< FLT (floating) Model fit (parameters and type).
-    pmSourceType type;   ///< Best identification of object.
-    float apMag;
-    float fitMag;
-}
-pmSource;
-
-
-/** pmPeakAlloc()
- *
- *  @return pmPeak*    newly allocated pmPeak with all internal pointers set to NULL
- */
-pmPeak *pmPeakAlloc(
-    int x,    ///< Row-coordinate in image space
-    int y,    ///< Col-coordinate in image space
-    float counts,   ///< The value of the peak pixel
-    pmPeakType class   ///< The type of peak pixel
-);
-
-
-/** pmMomentsAlloc()
- *
- */
-pmMoments *pmMomentsAlloc();
-
-
-/** pmModelAlloc()
- *
- */
-pmModel *pmModelAlloc(pmModelType type);
-
-
-/** pmSourceAlloc()
- *
- */
-pmSource  *pmSourceAlloc();
-
-
-/** pmFindVectorPeaks()
- *
- * Find all local peaks in the given vector above the given threshold. A peak
- * is defined as any element with a value greater than its two neighbors and with
- * a value above the threshold. Two types of special cases must be addressed.
- * Equal value elements: If an element has the same value as the following
- * element, it is not considered a peak. If an element has the same value as the
- * preceding element (but not the following), then it is considered a peak. Note
- * that this rule (arbitrarily) identifies flat regions by their trailing edge.
- * Edge cases: At start of the vector, the element must be higher than its
- * neighbor. At the end of the vector, the element must be higher or equal to its
- * neighbor. These two rules again places the peak associated with a flat region
- * which touches the image edge at the image edge. The result of this function is
- * a vector containing the coordinates (element number) of the detected peaks
- * (type psU32).
- *
- */
-psVector *pmFindVectorPeaks(
-    const psVector *vector,  ///< The input vector (float)
-    float threshold   ///< Threshold above which to find a peak
-);
-
-
-/** pmFindImagePeaks()
- *
- * Find all local peaks in the given image above the given threshold. This
- * function should find all row peaks using pmFindVectorPeaks, then test each row
- * peak and exclude peaks which are not local peaks. A peak is a local peak if it
- * has a higher value than all 8 neighbors. If the peak has the same value as its
- * +y neighbor or +x neighbor, it is NOT a local peak. If any other neighbors
- * have an equal value, the peak is considered a valid peak. Note two points:
- * first, the +x neighbor condition is already enforced by pmFindVectorPeaks.
- * Second, these rules have the effect of making flat-topped regions have single
- * peaks at the (+x,+y) corner. When selecting the peaks, their type must also be
- * set. The result of this function is an array of pmPeak entries.
- *
- */
-psArray *pmFindImagePeaks(
-    const psImage *image,  ///< The input image where peaks will be found (float)
-    float threshold   ///< Threshold above which to find a peak
-);
-
-
-/** pmCullPeaks()
- *
- * Eliminate peaks from the psList that have a peak value above the given
- * maximum, or fall outside the valid region.
- *
- */
-psList *pmCullPeaks(
-    psList *peaks,   ///< The psList of peaks to be culled
-    float maxValue,   ///< Cull peaks above this value
-    const psRegion valid                ///< Cull peaks otside this psRegion
-);
-
-
-/** pmPeaksSubset()
- *
- * Create a new peaks array, removing certain types of peaks from the input
- * array of peaks based on the given criteria. Peaks should be eliminated if they
- * have a peak value above the given maximum value limit or if the fall outside
- * the valid region.  The result of the function is a new array with a reduced
- * number of peaks.
- *
- */
-psArray *pmPeaksSubset(
-    psArray *peaks,                     ///< Add comment.
-    float maxvalue,                     ///< Add comment.
-    const psRegion valid                ///< Add comment.
-);
-
-
-/** pmSourceDefinePixels()
- *
- * Define psImage subarrays for the source located at coordinates x,y on the
- * image set defined by readout. The pixels defined by this operation consist of
- * a square window (of full width 2Radius+1) centered on the pixel which contains
- * the given coordinate, in the frame of the readout. The window is defined to
- * have limits which are valid within the boundary of the readout image, thus if
- * the radius would fall outside the image pixels, the subimage is truncated to
- * only consist of valid pixels. If readout->mask or readout->weight are not
- * NULL, matching subimages are defined for those images as well. This function
- * fails if no valid pixels can be defined (x or y less than Radius, for
- * example). This function should be used to define a region of interest around a
- * source, including both source and sky pixels.
- *
- * XXX: must code this.
- *
- */
-// XXX: Uncommenting the pmReadout causes compile errors.
-bool pmSourceDefinePixels(
-    pmSource *mySource,                 ///< Add comment.
-    pmReadout *readout,                 ///< Add comment.
-    psF32 x,                            ///< Add comment.
-    psF32 y,                            ///< Add comment.
-    psF32 Radius                        ///< Add comment.
-);
-
-
-/** pmSourceLocalSky()
- *
- * Measure the local sky in the vicinity of the given source. The Radius
- * defines the square aperture in which the moments will be measured. This
- * function assumes the source pixels have been defined, and that the value of
- * Radius here is smaller than the value of Radius used to define the pixels. The
- * annular region not contained within the radius defined here is used to measure
- * the local background in the vicinity of the source. The local background
- * measurement uses the specified statistic passed in via the statsOptions entry.
- * This function allocates the pmMoments structure. The resulting sky is used to
- * set the value of the pmMoments.sky element of the provided pmSource structure.
- *
- */
-bool pmSourceLocalSky(
-    pmSource *source,   ///< The input image (float)
-    psStatsOptions statsOptions, ///< The statistic used in calculating the background sky
-    float Radius   ///< The inner radius of the square annulus to exclude
-);
-
-
-/** pmSourceMoments()
- *
- * Measure source moments for the given source, using the value of
- * source.moments.sky provided as the local background value and the peak
- * coordinates as the initial source location. The resulting moment values are
- * applied to the source.moments entry, and the source is returned. The moments
- * are measured within the given circular radius of the source.peak coordinates.
- * The return value indicates the success (TRUE) of the operation.
- *
- */
-bool pmSourceMoments(
-    pmSource *source,   ///< The input pmSource for which moments will be computed
-    float radius   ///< Use a circle of pixels around the peak
-);
-
-
-/** pmSourcePSFClump()
- *
- * We use the source moments to make an initial, approximate source
- * classification, and as part of the information needed to build a PSF model for
- * the image. As long as the PSF shape does not vary excessively across the
- * image, the sources which are represented by a PSF (the start) will have very
- * similar second moments. The function pmSourcePSFClump searches a collection of
- * sources with measured moments for a group with moments which are all very
- * similar. The function returns a pmPSFClump structure, representing the
- * centroid and size of the clump in the sigma_x, sigma_y second-moment plane.
- *
- * The goal is to identify and characterize the stellar clump within the
- * sigma_x, sigma_y second-moment plane.  To do this, an image is constructed to
- * represent this plane.  The units of sigma_x and sigma_y are in image pixels. A
- * pixel in this analysis image represents 0.1 pixels in the input image. The
- * dimensions of the image need only be 10 pixels. The peak pixel in this image
- * (above a threshold of half of the image maximum) is found. The coordinates of
- * this peak pixel represent the 2D mode of the sigma_x, sigma_y distribution.
- * The sources with sigma_x, sigma_y within 0.2 pixels of this value are then
- *  * used to calculate the median and standard deviation of the sigma_x, sigma_y
- * values. These resulting values are returned via the pmPSFClump structure.
- *
- * The return value indicates the success (TRUE) of the operation.
- *
- * XXX: Limit the S/N of the candidate sources (part of Metadata)? (TBD).
- * XXX: Save the clump parameters on the Metadata (TBD)
- *
- */
-pmPSFClump pmSourcePSFClump(
-    psArray *source,   ///< The input pmSource
-    psMetadata *metadata  ///< Contains classification parameters
-);
-
-
-/** pmSourceRoughClass()
- *
- * Based on the specified data values, make a guess at the source
- * classification. The sources are provides as a psArray of pmSource entries.
- * Definable parameters needed to make the classification are provided to the
- * routine with the psMetadata structure. The rules (in SDRS) refer to values which
- * can be extracted from the metadata using the given keywords. Except as noted,
- * the data type for these parameters are psF32.
- *
- */
-bool pmSourceRoughClass(
-    psArray *source,   ///< The input pmSource
-    psMetadata *metadata,  ///< Contains classification parameters
-    pmPSFClump clump   ///< Statistics about the PSF clump
-);
-
-
-/** pmSourceModelGuess()
- *
- * Convert available data to an initial guess for the given model. This
- * function allocates a pmModel entry for the pmSource structure based on the
- * provided model selection. The method of defining the model parameter guesses
- * are specified for each model below. The guess values are placed in the model
- * parameters. The function returns TRUE on success or FALSE on failure.
- *
- */
-pmModel *pmSourceModelGuess(
-    pmSource *source,   ///< The input pmSource
-    pmModelType model   ///< The type of model to be created.
-);
-
-
-/** pmContourType
- *
- * Only one type is defined at present.
- *
- */
-typedef enum {
-    PS_CONTOUR_CRUDE,
-    PS_CONTOUR_UNKNOWN01,
-    PS_CONTOUR_UNKNOWN02
-} pmContourType;
-
-
-/** pmSourceContour()
- *
- * Find points in a contour for the given source at the given level. If type
- * is PM_CONTOUR_CRUDE, the contour is found by starting at the source peak,
- * running along each pixel row until the level is crossed, then interpolating to
- * the level coordinate for that row. This is done for each row, with the
- * starting point determined by the midpoint of the previous row, until the
- * starting point has a value below the contour level. The returned contour
- * consists of two vectors giving the x and y coordinates of the contour levels.
- * This function may be used as part of the model guess inputs.  Other contour
- * types may be specified in the future for more refined contours (TBD)
- *
- */
-psArray *pmSourceContour(
-    pmSource *source,   ///< The input pmSource
-    const psImage *image,  ///< The input image (float) (this arg should be removed)
-    float level,   ///< The level of the contour
-    pmContourType mode   ///< Currently this must be PS_CONTOUR_CRUDE
-);
-
-
-/** pmSourceFitModel()
- *
- * Fit the requested model to the specified source. The starting guess for the
- * model is given by the input source.model parameter values. The pixels of
- * interest are specified by the source.pixelsand source.maskentries. This
- * function calls psMinimizeLMChi2() on the image data. The function returns TRUE
- * on success or FALSE on failure.
- *
- */
-bool pmSourceFitModel(
-    pmSource *source,   ///< The input pmSource
-    pmModel *model,   ///< model to be fitted
-    const bool PSF   ///< Treat model as PSF or FLT?
-);
-
-
-/** pmModelFitStatus()
- *
- * This function wraps the call to the model-specific function returned by
- * pmModelFitStatusFunc_GetFunction.  The model-specific function examines the
- * model parameters, parameter errors, Chisq, S/N, and other parameters available
- * from model to decide if the particular fit was successful or not.
- *
- * XXX: Must code this.
- *
- */
-bool pmModelFitStatus(
-    pmModel *model                      ///< Add comment.
-);
-
-
-/** pmSourceAddModel()
- *
- * Add the given source model flux to/from the provided image. The boolean
- * option center selects if the source is re-centered to the image center or if
- * it is placed at its centroid location. The boolean option sky selects if the
- * background sky is applied (TRUE) or not. The pixel range in the target image
- * is at most the pixel range specified by the source.pixels image. The success
- * status is returned.
- *
- */
-bool pmSourceAddModel(
-    psImage *image,   ///< The output image (float)
-    psImage *mask,   ///< The image pixel mask (valid == 0)
-    pmModel *model,   ///< The input pmModel
-    bool center,    ///< A boolean flag that determines whether pixels are centered
-    bool sky        ///< A boolean flag that determines if the sky is subtracted
-);
-
-
-/** pmSourceSubModel()
- *
- * Subtract the given source model flux to/from the provided image. The
- * boolean option center selects if the source is re-centered to the image center
- * or if it is placed at its centroid location. The boolean option sky selects if
- * the background sky is applied (TRUE) or not. The pixel range in the target
- * image is at most the pixel range specified by the source.pixels image. The
- * success status is returned.
- *
- */
-bool pmSourceSubModel(
-    psImage *image,   ///< The output image (float)
-    psImage *mask,   ///< The image pixel mask (valid == 0)
-    pmModel *model,   ///< The input pmModel
-    bool center,    ///< A boolean flag that determines whether pixels are centered
-    bool sky        ///< A boolean flag that determines if the sky is subtracted
-);
-
-
-/**
- *
- * The function returns both the magnitude of the fit, defined as -2.5log(flux),
- * where the flux is integrated under the model, theoretically from a radius of 0
- * to infinity. In practice, we integrate the model beyond 50sigma.  The aperture magnitude is
- * defined as -2.5log(flux) , where the flux is summed for all pixels which are
- * not excluded by the aperture mask. The model flux is calculated by calling the
- * model-specific function provided by pmModelFlux_GetFunction.
- *
- * XXX: must code this.
- *
- */
-bool pmSourcePhotometry(
-    float *fitMag,                      ///< integrated fit magnitude
-    float *obsMag,   ///< aperture flux magnitude
-    pmModel *model,                     ///< model used for photometry
-    psImage *image,                     ///< image pixels to be used
-    psImage *mask                       ///< mask of pixels to ignore
-);
-
 
 /**
@@ -563,4 +32,5 @@
  * This function converts the source classification into the closest available
  * approximation to the Dophot classification scheme:
+ * XXX EAM : fix this to use current source classification scheme
  *
  * PM_SOURCE_DEFECT: 8
@@ -598,60 +68,4 @@
 );
 
-/** pmSourceFitModel_v5()
- *
- * Fit the requested model to the specified source. The starting guess for the
- * model is given by the input source.model parameter values. The pixels of
- * interest are specified by the source.pixelsand source.maskentries. This
- * function calls psMinimizeLMChi2() on the image data. The function returns TRUE
- * on success or FALSE on failure.
- *
- */
-bool pmSourceFitModel_v5(
-    pmSource *source,   ///< The input pmSource
-    pmModel *model,   ///< model to be fitted
-    const bool PSF   ///< Treat model as PSF or FLT?
-);
-
-
-/** pmSourceFitModel_v7()
- *
- * Fit the requested model to the specified source. The starting guess for the
- * model is given by the input source.model parameter values. The pixels of
- * interest are specified by the source.pixelsand source.maskentries. This
- * function calls psMinimizeLMChi2() on the image data. The function returns TRUE
- * on success or FALSE on failure.
- *
- */
-bool pmSourceFitModel_v7(
-    pmSource *source,   ///< The input pmSource
-    pmModel *model,   ///< model to be fitted
-    const bool PSF   ///< Treat model as PSF or FLT?
-);
-
-
-/** pmSourcePhotometry()
- *
- * XXX: Need descriptions
- *
- */
-bool pmSourcePhotometry(
-    float *fitMag,
-    float *obsMag,
-    pmModel *model,
-    psImage *image,
-    psImage *mask
-);
-
-/** pmModelEval()
- *
- *  XXX: Need descriptions
- *
- */
-psF32 pmModelEval(
-    pmModel *model,
-    psImage *image,
-    psS32 col,
-    psS32 row
-);
 
 #endif
Index: trunk/psModules/src/objects/pmPSF.h
===================================================================
--- trunk/psModules/src/objects/pmPSF.h	(revision 6529)
+++ trunk/psModules/src/objects/pmPSF.h	(revision 6872)
@@ -6,6 +6,6 @@
  *  @author EAM, IfA
  *
- *  @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-10-10 19:53:40 $
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -15,5 +15,4 @@
 # ifndef PM_PSF_H
 # define PM_PSF_H
-
 
 /** pmPSF data structure
@@ -33,12 +32,29 @@
     pmModelType type;   ///< PSF Model in use
     psArray *params;   ///< Model parameters (psPolynomial2D)
-    float chisq;   ///< PSF goodness statistic
-    float ApResid;                      ///< ???
-    float dApResid;                     ///< ???
-    float skyBias;                      ///< ???
+    psPolynomial1D *ChiTrend;  ///< Chisq vs flux fit (correction for systematic errors)
+    psPolynomial4D *ApTrend;  ///< ApResid vs (x,y,rflux) (rflux = ten(0.4*mInst)
+    pmGrowthCurve *growth;  ///< apMag vs Radius
+    float ApResid;   ///< apMag - psfMag (for PSF stars)
+    float dApResid;   ///< scatter of ApResid
+    float skyBias;   ///< implied residual sky offset from ApResid fit
+    float skySat;   ///< roll-over of ApResid fit
+    float chisq;   ///< PSF goodness statistic (unused??)
     int nPSFstars;   ///< number of stars used to measure PSF
+    int nApResid;   ///< number of stars used to measure ApResid
+    bool poissonErrors;
 }
 pmPSF;
 
+typedef enum {
+    PM_PSF_NONE,
+    PM_PSF_CONSTANT,
+    PM_PSF_SKYBIAS,
+    PM_PSF_SKYSAT,
+    PM_PSF_XY_LIN,
+    PM_PSF_XY_QUAD,
+    PM_PSF_SKY_XY_LIN,
+    PM_PSF_SKYSAT_XY_LIN,
+    PM_PSF_ALL
+} pmPSF_ApTrendOptions;
 
 /**
@@ -48,5 +64,6 @@
  */
 pmPSF *pmPSFAlloc(
-    pmModelType type                    ///< Add comment
+    pmModelType type,   // type of model for PSF
+    bool poissonErrors   ///< use poissonian errors or not?
 );
 
@@ -85,3 +102,5 @@
 );
 
+bool pmPSF_MaskApTrend (pmPSF *psf, pmPSF_ApTrendOptions option);
+
 # endif
Index: trunk/psModules/src/objects/pmPSF_IO.c
===================================================================
--- trunk/psModules/src/objects/pmPSF_IO.c	(revision 6872)
+++ trunk/psModules/src/objects/pmPSF_IO.c	(revision 6872)
@@ -0,0 +1,312 @@
+/** @file  pmPSF_IO.c
+ *
+ * This file contains functions to read and write PSF models using the psMetadata Config file
+ * format.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+/*****************************************************************************/
+/* INCLUDE FILES                                                             */
+/*****************************************************************************/
+
+#include <pslib.h>
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAfile.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmPSF_IO.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+psMetadata *pmPSFtoMD (psMetadata *metadata, pmPSF *psf)
+{
+
+    if (metadata == NULL) {
+        metadata = psMetadataAlloc ();
+    }
+
+    char *modelName = pmModelGetType (psf->type);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_MODEL_NAME", PS_DATA_STRING, "PSF model name", modelName);
+
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_POISSON_ERRORS", PS_DATA_BOOL, "Poisson errors for fits", psf->poissonErrors);
+
+    int nPar = pmModelParameterCount (psf->type)    ;
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_MODEL_NPAR", PS_DATA_S32, "PSF model parameter count", nPar);
+
+    for (int i = 0; i < nPar - 4; i++) {
+        psPolynomial2D *poly = psf->params->data[i];
+        psPolynomial2DtoMD (metadata, poly, "PSF_PAR%02d", i);
+    }
+    psPolynomial4DtoMD (metadata, psf->ApTrend, "APTREND");
+
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_AP_RESID", PS_DATA_F32, "aperture residual", psf->ApResid);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_dAP_RESID", PS_DATA_F32, "aperture residual scatter", psf->dApResid);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_SKY_BIAS", PS_DATA_F32, "sky bias level", psf->skyBias);
+
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_CHISQ", PS_DATA_F32, "chi-square for fit", psf->chisq);
+    psMetadataAdd (metadata, PS_LIST_TAIL, "PSF_NSTARS", PS_DATA_S32, "number of stars used to measure PSF", psf->nPSFstars);
+
+    return metadata;
+}
+
+pmPSF *pmPSFfromMD (psMetadata *metadata)
+{
+
+    bool status;
+    char keyword[80];
+
+    char *modelName = psMetadataLookupPtr (&status, metadata, "PSF_MODEL_NAME");
+    pmModelType type = pmModelSetType (modelName);
+
+    bool poissonErrors = psMetadataLookupPtr (&status, metadata, "PSF_POISSON_ERRORS");
+    if (!status)
+        poissonErrors = true;
+
+    pmPSF *psf = pmPSFAlloc (type, poissonErrors);
+
+    int nPar = psMetadataLookupS32 (&status, metadata, "PSF_MODEL_NPAR");
+    if (nPar != pmModelParameterCount (psf->type))
+        psAbort ("read PSF" , "mismatch model par count");
+
+    for (int i = 0; i < nPar - 4; i++) {
+        sprintf (keyword, "PSF_PAR%02d", i);
+        psMetadata *folder = psMetadataLookupPtr (&status, metadata, keyword);
+        psPolynomial2D *poly = psPolynomial2DfromMD (folder);
+        psFree (psf->params->data[i]);
+        psf->params->data[i] = poly;
+    }
+    sprintf (keyword, "APTREND");
+    psMetadata *folder = psMetadataLookupPtr (&status, metadata, keyword);
+    psPolynomial4D *poly = psPolynomial4DfromMD (folder);
+    psFree (psf->ApTrend);
+    psf->ApTrend = poly;
+
+    psf->ApResid = psMetadataLookupF32 (&status, metadata, "PSF_AP_RESID");
+    psf->dApResid = psMetadataLookupF32 (&status, metadata, "PSF_dAP_RESID");
+    psf->skyBias = psMetadataLookupF32 (&status, metadata, "PSF_SKY_BIAS");
+
+    psf->chisq = psMetadataLookupF32 (&status, metadata, "PSF_CHISQ");
+    psf->nPSFstars = psMetadataLookupS32 (&status, metadata, "PSF_NSTARS");
+
+    psFree (metadata);
+    return (psf);
+}
+
+bool pmFPAviewWritePSFmodel (const pmFPAview *view, pmFPAfile *file)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWritePSFmodel (fpa, view, file);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipWritePSFmodel (chip, view, file);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWritePSFmodel (cell, view, file);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutWritePSFmodel (readout, view, file);
+    return true;
+}
+
+// read in all chip-level PSFmodel files for this FPA
+bool pmFPAWritePSFmodel (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipWritePSFmodel (chip, view, file);
+    }
+    return true;
+}
+
+// read in all cell-level PSFmodel files for this chip
+bool pmChipWritePSFmodel (pmChip *chip, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellWritePSFmodel (cell, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level PSFmodel files for this cell
+bool pmCellWritePSFmodel (pmCell *cell, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutWritePSFmodel (readout, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutWritePSFmodel (pmReadout *readout, const pmFPAview *view, pmFPAfile *file)
+{
+    bool status;
+    char *filename;
+
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+
+    switch (file->type) {
+    case PM_FPA_FILE_PSF:
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+        psMetadata *psfData = pmPSFtoMD (NULL, psf);
+        psMetadataConfigWrite (psfData, filename);
+        psFree (psfData);
+        psFree (filename);
+        return true;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    return false;
+}
+
+
+
+bool pmFPAviewReadPSFmodel (const pmFPAview *view, pmFPAfile *file)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadPSFmodel (fpa, view, file);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipReadPSFmodel (chip, view, file);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellReadPSFmodel (cell, view, file);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutReadPSFmodel (readout, view, file);
+    return true;
+}
+
+// read in all chip-level PSFmodel files for this FPA
+bool pmFPAReadPSFmodel (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipReadPSFmodel (chip, view, file);
+    }
+    return true;
+}
+
+// read in all cell-level PSFmodel files for this chip
+bool pmChipReadPSFmodel (pmChip *chip, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellReadPSFmodel (cell, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level PSFmodel files for this cell
+bool pmCellReadPSFmodel (pmCell *cell, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutReadPSFmodel (readout, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutReadPSFmodel (pmReadout *readout, const pmFPAview *view, pmFPAfile *file)
+{
+
+    int Nfail;
+    char *filename;
+
+    switch (file->type) {
+    case PM_FPA_FILE_PSF:
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+
+        psMetadata *psfData = psMetadataConfigParse (NULL, &Nfail, filename, FALSE);
+        pmPSF *psf = pmPSFfromMD (psfData);
+        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.PSF", PS_DATA_UNKNOWN, "psphot psf", psf);
+
+        psFree (psf);
+        psFree (psfData);
+        psFree (filename);
+
+        return true;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    return false;
+}
+
Index: trunk/psModules/src/objects/pmPSF_IO.h
===================================================================
--- trunk/psModules/src/objects/pmPSF_IO.h	(revision 6872)
+++ trunk/psModules/src/objects/pmPSF_IO.h	(revision 6872)
@@ -0,0 +1,32 @@
+/** @file  pmPSF.h
+ *
+ * This file contains typedefs for the Point-Spread Function and prototypes
+ * for functions that calculate the PSF.
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_PSF_IO_H
+# define PM_PSF_IO_H
+
+psMetadata *pmPSFtoMD (psMetadata *metadata, pmPSF *psf);
+pmPSF *pmPSFfromMD (psMetadata *metadata);
+bool pmFPAviewWritePSFmodel (const pmFPAview *view, pmFPAfile *file);
+bool pmFPAWritePSFmodel (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file);
+bool pmChipWritePSFmodel (pmChip *chip, const pmFPAview *view, pmFPAfile *file);
+bool pmCellWritePSFmodel (pmCell *cell, const pmFPAview *view, pmFPAfile *file);
+bool pmReadoutWritePSFmodel (pmReadout *readout, const pmFPAview *view, pmFPAfile *file);
+
+bool pmFPAviewReadPSFmodel (const pmFPAview *view, pmFPAfile *file);
+bool pmFPAReadPSFmodel (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file);
+bool pmChipReadPSFmodel (pmChip *chip, const pmFPAview *view, pmFPAfile *file);
+bool pmCellReadPSFmodel (pmCell *cell, const pmFPAview *view, pmFPAfile *file);
+bool pmReadoutReadPSFmodel (pmReadout *readout, const pmFPAview *view, pmFPAfile *file);
+
+# endif
Index: trunk/psModules/src/objects/pmPSFtry.h
===================================================================
--- trunk/psModules/src/objects/pmPSFtry.h	(revision 6529)
+++ trunk/psModules/src/objects/pmPSFtry.h	(revision 6872)
@@ -6,6 +6,6 @@
  *  @author EAM, IfA
  *
- *  @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
- *  @date $Date: 2005-12-24 01:24:32 $
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
  *
  *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
@@ -18,11 +18,11 @@
 
 /**
- *
+ * 
  * This structure contains a pointer to the collection of sources which will
  * be used to test the PSF model form. It lists the pmModelType type of model
  * being tests, and contains an element to store the resulting psf
  * representation. In addition, this structure carries the complete collection of
- * FLT (floating parameter) and PSF (fixed parameter) model fits to each of the
- * sources modelFLT and modelPSF. It also contains a mask which is set by the
+ * EXT (floating parameter) and PSF (fixed parameter) model fits to each of the
+ * sources modelEXT and modelPSF. It also contains a mask which is set by the
  * model fitting and psf fitting steps. For each model, the value of the quality
  * metric is stored in the vector metric and the fitted instrumental magnitude is
@@ -38,9 +38,9 @@
  * ultimate metric to intercompare multiple types of PSF models is the value of
  * the aperture correction scatter.
- *
+ * 
  * XXX: There are many more members in the SDRS then in the prototype code.
  * I stuck with the prototype code.
- *
- *
+ * 
+ * 
  */
 typedef struct
@@ -48,5 +48,5 @@
     pmPSF      *psf;                    ///< Add comment.
     psArray    *sources;                ///< pointers to the original sources
-    psArray    *modelFLT;               ///< model fits, floating parameters
+    psArray    *modelEXT;               ///< model fits, floating parameters
     psArray    *modelPSF;               ///< model fits, PSF parameters
     psVector   *mask;                   ///< Add comment.
@@ -58,13 +58,13 @@
 
 /** pmPSFtryMaskValues
- *
+ * 
  * The following datatype defines the masks used by the pmPSFtry analysis to
  * identify sources which should or should not be included in the analysis.
- *
+ * 
  */
-enum {
+typedef enum {
     PSFTRY_MASK_CLEAR    = 0x00,        ///< Add comment.
     PSFTRY_MASK_OUTLIER  = 0x01,        ///< 1: outlier in psf polynomial fit (provided by psPolynomials)
-    PSFTRY_MASK_FLT_FAIL = 0x02,        ///< 2: flt model failed to converge
+    PSFTRY_MASK_EXT_FAIL = 0x02,        ///< 2: ext model failed to converge
     PSFTRY_MASK_PSF_FAIL = 0x04,        ///< 3: psf model failed to converge
     PSFTRY_MASK_BAD_PHOT = 0x08,        ///< 4: invalid source photometry
@@ -74,33 +74,35 @@
 
 /** pmPSFtryAlloc()
- *
+ * 
  * Allocate a pmPSFtry data structure.
- *
+ * 
  */
 pmPSFtry *pmPSFtryAlloc(
     psArray *stars,                     ///< Add comment.
-    char *modelName                     ///< Add comment.
+    char *modelName,                     ///< Add comment.
+    bool poissonErrors   // use poissonian or constant errors?
 );
 
 
 /** pmPSFtryModel()
- *
+ * 
  * This function takes the input collection of sources and performs a complete
  * analysis to determine a PSF model of the given type (specified by model name).
  * The result is a pmPSFtry with the results of the analysis.
- *
+ * 
  */
 pmPSFtry *pmPSFtryModel(
     psArray *sources,                   ///< Add comment.
     char *modelName,                    ///< Add comment.
-    float radius                        ///< Add comment.
+    float radius,                     ///< Add comment.
+    bool poissonErrors   // use poissonian or constant errors?
 );
 
 
 /** pmPSFtryMetric()
- *
+ * 
  * This function is used to measure the PSF model metric for the set of
  * results contained in the pmPSFtry structure.
- *
+ * 
  */
 bool pmPSFtryMetric(
Index: trunk/psModules/src/objects/pmPeaks.c
===================================================================
--- trunk/psModules/src/objects/pmPeaks.c	(revision 6872)
+++ trunk/psModules/src/objects/pmPeaks.c	(revision 6872)
@@ -0,0 +1,542 @@
+/** @file  pmPeaks.c
+ *
+ *  This file defines functions to detect and manipulate peaks in images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmPeaks.h"
+
+/******************************************************************************
+myListAddPeak(): A private function which allocates a psArray, if the list
+argument is NULL, otherwise it adds the peak to that list.
+XXX EAM : changed the output to psArray
+XXX EAM : Switched row, col args
+XXX EAM : NOTE: this was changed in the call, so the new code is consistent
+*****************************************************************************/
+static psArray *myListAddPeak(psArray *list,
+                              psS32 row,
+                              psS32 col,
+                              psF32 counts,
+                              pmPeakType type)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    pmPeak *tmpPeak = pmPeakAlloc(col, row, counts, type);
+
+    if (list == NULL) {
+        list = psArrayAlloc(100);
+        list->n = 0;
+    }
+    psArrayAdd(list, 100, tmpPeak);
+    psFree (tmpPeak);
+    // XXX EAM : is this free appropriate?  (does psArrayAdd increment memory counter?)
+
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+    return(list);
+}
+
+/******************************************************************************
+getRowVectorFromImage(): a private function which simply returns a
+psVector containing the specified row of data from the psImage.
+ 
+XXX: Is there a better way to do this?  
+XXX EAM: does this really need to alloc a new vector???
+*****************************************************************************/
+static psVector *getRowVectorFromImage(psImage *image,
+                                       psU32 row)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+
+    psVector *tmpVector = psVectorAlloc(image->numCols, PS_TYPE_F32);
+    for (psU32 col = 0; col < image->numCols ; col++) {
+        tmpVector->data.F32[col] = image->data.F32[row][col];
+    }
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+    return(tmpVector);
+}
+
+/******************************************************************************
+isItInThisRegion(): a private function which simply returns a
+boolean denoting if specified coordinate is in the region.
+XXX: Macro this.
+*****************************************************************************/
+static bool isItInThisRegion(const psRegion valid,
+                             psS32 x,
+                             psS32 y)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    if ((x >= valid.x0) &&
+            (x <= valid.x1) &&
+            (y >= valid.y0) &&
+            (y <= valid.y1)) {
+        psTrace(__func__, 4, "---- %s(true) end ----\n", __func__);
+        return(true);
+    }
+    psTrace(__func__, 4, "---- %s(false) end ----\n", __func__);
+    return(false);
+}
+
+/******************************************************************************
+pmPeakAlloc(): Allocate the pmPeak data structure and set appropriate members.
+*****************************************************************************/
+pmPeak *pmPeakAlloc(psS32 x,
+                    psS32 y,
+                    psF32 counts,
+                    pmPeakType type)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmPeak *tmp = (pmPeak *) psAlloc(sizeof(pmPeak));
+    tmp->x = x;
+    tmp->y = y;
+    tmp->counts = counts;
+    tmp->type = type;
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+// psSort comparison function for peaks
+int pmPeaksCompareAscend (const void **a, const void **b)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->counts - B->counts;
+    if (diff < FLT_EPSILON) {
+        psTrace(__func__, 3, "---- %s(-1) end ----\n", __func__);
+        return (-1);
+    } else if (diff > FLT_EPSILON) {
+        psTrace(__func__, 3, "---- %s(+1) end ----\n", __func__);
+        return (+1);
+    }
+    psTrace(__func__, 3, "---- %s(0) end ----\n", __func__);
+    return (0);
+}
+
+// psSort comparison function for peaks
+int pmPeaksCompareDescend (const void **a, const void **b)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmPeak *A = *(pmPeak **)a;
+    pmPeak *B = *(pmPeak **)b;
+
+    psF32 diff;
+
+    diff = A->counts - B->counts;
+    if (diff < FLT_EPSILON) {
+        psTrace(__func__, 3, "---- %s(+1) end ----\n", __func__);
+        return (+1);
+    } else if (diff > FLT_EPSILON) {
+        psTrace(__func__, 3, "---- %s(-1) end ----\n", __func__);
+        return (-1);
+    }
+    psTrace(__func__, 3, "---- %s(0) end ----\n", __func__);
+    return (0);
+}
+
+/******************************************************************************
+pmFindVectorPeaks(vector, threshold): Find all local peaks in the given vector
+above the given threshold.  Returns a vector of type PS_TYPE_U32 containing
+the location (x value) of all peaks.
+ 
+XXX: What types should be supported?  Only F32 is implemented.
+ 
+XXX: We currently step through the input vector twice; once to determine the
+size of the output vector, then to set the values of the output vector.
+Depending upon actual use, this may need to be optimized.
+*****************************************************************************/
+psVector *pmFindVectorPeaks(const psVector *vector,
+                            psF32 threshold)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_VECTOR_NON_NULL(vector, NULL);
+    PS_ASSERT_VECTOR_NON_EMPTY(vector, NULL);
+    PS_ASSERT_VECTOR_TYPE(vector, PS_TYPE_F32, NULL);
+    int count = 0;
+    int n = vector->n;
+
+    //
+    // Special case: the input vector has a single element.
+    //
+    if (n == 1) {
+        psVector *tmpVector = NULL;
+        ;
+        if (vector->data.F32[0] > threshold) {
+            tmpVector = psVectorAlloc(1, PS_TYPE_U32);
+            tmpVector->data.U32[0] = 0;
+        } else {
+            tmpVector = psVectorAlloc(0, PS_TYPE_U32);
+        }
+        psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+        return(tmpVector);
+    }
+
+    //
+    // Determine if first pixel is a peak
+    //
+    if ((vector->data.F32[0] > vector->data.F32[1]) &&
+            (vector->data.F32[0] > threshold)) {
+        count++;
+    }
+
+    //
+    // Determine if interior pixels are peaks
+    //
+    for (psU32 i = 1; i < n-1 ; i++) {
+        if ((vector->data.F32[i] > vector->data.F32[i-1]) &&
+                (vector->data.F32[i] > vector->data.F32[i+1]) &&
+                (vector->data.F32[i] > threshold)) {
+            count++;
+        }
+    }
+
+    //
+    // Determine if last pixel is a peak
+    //
+    if ((vector->data.F32[n-1] > vector->data.F32[n-2]) &&
+            (vector->data.F32[n-1] > threshold)) {
+        count++;
+    }
+
+    //
+    // We know how many peaks exist, so we now allocate a psVector to store
+    // those peaks.
+    //
+    psVector *tmpVector = psVectorAlloc(count, PS_TYPE_U32);
+    count = 0;
+
+    //
+    // Determine if first pixel is a peak
+    //
+    if ((vector->data.F32[0] > vector->data.F32[1]) &&
+            (vector->data.F32[0] > threshold)) {
+        tmpVector->data.U32[count++] = 0;
+    }
+
+    //
+    // Determine if interior pixels are peaks
+    //
+    for (psU32 i = 1; i < (n-1) ; i++) {
+        if ((vector->data.F32[i] > vector->data.F32[i-1]) &&
+                (vector->data.F32[i] > vector->data.F32[i+1]) &&
+                (vector->data.F32[i] > threshold)) {
+            tmpVector->data.U32[count++] = i;
+        }
+    }
+
+    //
+    // Determine if last pixel is a peak
+    //
+    if ((vector->data.F32[n-1] > vector->data.F32[n-2]) &&
+            (vector->data.F32[n-1] > threshold)) {
+        tmpVector->data.U32[count++] = n-1;
+    }
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmpVector);
+}
+
+
+/******************************************************************************
+pmFindImagePeaks(image, threshold): Find all local peaks in the given psImage
+above the given threshold.  Returns a psArray containing location (x/y value)
+of all peaks.
+ 
+XXX: I'm not convinced the peak type definition in the SDRS is mutually
+exclusive.  Some peaks can have multiple types.  Edges for sure.  Also, a
+digonal line with the same value at each point will have a peak for every
+point on that line.
+ 
+XXX: This does not work if image has either a single row, or a single column.
+ 
+XXX: In the output psArray elements, should we use the image row/column offsets?
+     Currently, we do not.
+XXX EAM : this function needs to return peaks in *parent* coords
+ 
+XXX: Merge with CVS 1.20.  This had the proper code for images with a single
+row or column.
+ 
+*****************************************************************************/
+psArray *pmFindImagePeaks(const psImage *image,
+                          psF32 threshold)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_IMAGE_NON_NULL(image, NULL);
+    PS_ASSERT_IMAGE_TYPE(image, PS_TYPE_F32, NULL);
+    if ((image->numRows == 1) || (image->numCols == 1)) {
+        psError(PS_ERR_UNKNOWN, true, "Currently, input image must have at least 2 rows and 2 columns.");
+        psTrace(__func__, 3, "---- %s(NULL) end ----\n", __func__);
+        return(NULL);
+    }
+    psVector *tmpRow = NULL;
+    psU32 col = 0;
+    psU32 row = 0;
+    psArray *list = NULL;
+
+    psU32 col0 = image->col0;
+    psU32 row0 = image->row0;
+
+    //
+    // Find peaks in row 0 only.
+    //
+    row = 0;
+    tmpRow = getRowVectorFromImage((psImage *) image, row);
+    psVector *row1 = pmFindVectorPeaks(tmpRow, threshold);
+    // pmFindVectorPeaks returns coords in the vector, not corrected for col0
+
+    for (psU32 i = 0 ; i < row1->n ; i++ ) {
+        col = row1->data.U32[i];
+        //
+        // Determine if pixel (0,0) is a peak.
+        //
+        if (col == 0) {
+            if ( (image->data.F32[row][col] >  image->data.F32[row][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+        } else if (col < (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col+1]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+
+        } else if (col == (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] > image->data.F32[row+1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row+1][col-1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "peak specified valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+
+    //
+    // Exit if this image has a single row.
+    //
+    if (image->numRows == 1) {
+        psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+        return(list);
+    }
+
+    //
+    // Find peaks in interior rows only.
+    //
+    for (row = 1 ; row < (image->numRows - 1) ; row++) {
+        tmpRow = getRowVectorFromImage((psImage *) image, row);
+        row1 = pmFindVectorPeaks(tmpRow, threshold);
+
+        // Step through all local peaks in this row.
+        for (psU32 i = 0 ; i < row1->n ; i++ ) {
+            pmPeakType myType = PM_PEAK_UNDEF;
+            col = row1->data.U32[i];
+
+            if (col == 0) {
+                // If col==0, then we can not read col-1 pixels
+                if ((image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                    myType = PM_PEAK_EDGE;
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], myType);
+                }
+            } else if (col < (image->numCols - 1)) {
+                // This is an interior pixel
+                if ((image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                        (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                        (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col+1])) {
+                    if (image->data.F32[row][col] > threshold) {
+                        if ((image->data.F32[row][col] > image->data.F32[row-1][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row-1][col]) &&
+                                (image->data.F32[row][col] > image->data.F32[row-1][col+1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row][col+1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col-1]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col]) &&
+                                (image->data.F32[row][col] > image->data.F32[row+1][col+1])) {
+                            myType = PM_PEAK_LONE;
+                        }
+
+                        if ((image->data.F32[row][col] == image->data.F32[row-1][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row-1][col]) ||
+                                (image->data.F32[row][col] == image->data.F32[row-1][col+1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row][col+1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col-1]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col]) ||
+                                (image->data.F32[row][col] == image->data.F32[row+1][col+1])) {
+                            myType = PM_PEAK_FLAT;
+                        }
+
+                        list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], myType);
+                    }
+                }
+            } else if (col == (image->numCols - 1)) {
+                // If col==numCols - 1, then we can not read col+1 pixels
+                if ((image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                        (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                        (image->data.F32[row][col] > image->data.F32[row][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row][col+1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col-1]) &&
+                        (image->data.F32[row][col] >= image->data.F32[row+1][col])) {
+                    myType = PM_PEAK_EDGE;
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], myType);
+                }
+            } else {
+                psError(PS_ERR_UNKNOWN, true, "peak specified outside valid column range.");
+            }
+
+        }
+        psFree (tmpRow);
+        psFree (row1);
+    }
+
+    //
+    // Find peaks in the last row only.
+    //
+    row = image->numRows - 1;
+    tmpRow = getRowVectorFromImage((psImage *) image, row);
+    row1 = pmFindVectorPeaks(tmpRow, threshold);
+    for (psU32 i = 0 ; i < row1->n ; i++ ) {
+        col = row1->data.U32[i];
+        if (col == 0) {
+            if ( (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+        } else if (col < (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row-1][col+1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col-1]) &&
+                    (image->data.F32[row][col] >= image->data.F32[row][col+1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+
+        } else if (col == (image->numCols - 1)) {
+            if ( (image->data.F32[row][col] >= image->data.F32[row-1][col-1]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row-1][col]) &&
+                    (image->data.F32[row][col] >  image->data.F32[row][col-1])) {
+                if (image->data.F32[row][col] > threshold) {
+                    list = myListAddPeak(list, row + row0, col + col0, image->data.F32[row][col], PM_PEAK_EDGE);
+                }
+            }
+        } else {
+            psError(PS_ERR_UNKNOWN, true, "peak specified outside valid column range.");
+        }
+    }
+    psFree (tmpRow);
+    psFree (row1);
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(list);
+}
+
+
+/******************************************************************************
+psCullPeaks(peaks, maxValue, valid): eliminate peaks from the psArray that have
+a peak value above the given maximum, or fall outside the valid region.
+ 
+XXX: Should the sky value be used when comparing the maximum?
+ 
+XXX: warning message if valid is NULL?
+ 
+XXX: changed API to create a NEW output psArray (should change name as well)
+ 
+XXX: Do we free the psList elements of those culled peaks?
+ 
+XXX EAM : do we still need pmCullPeaks, or only pmPeaksSubset?
+*****************************************************************************/
+psList *pmCullPeaks(psList *peaks,
+                    psF32 maxValue,
+                    const psRegion valid)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(peaks, NULL);
+
+    psListElem *tmpListElem = (psListElem *) peaks->head;
+    psS32 indexNum = 0;
+
+    //    printf("pmCullPeaks(): list size is %d\n", peaks->size);
+    while (tmpListElem != NULL) {
+        pmPeak *tmpPeak = (pmPeak *) tmpListElem->data;
+        if ((tmpPeak->counts > maxValue) ||
+                (true == isItInThisRegion(valid, tmpPeak->x, tmpPeak->y))) {
+            psListRemoveData(peaks, (psPtr) tmpPeak);
+        }
+
+        indexNum++;
+        tmpListElem = tmpListElem->next;
+    }
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(peaks);
+}
+
+// XXX EAM: I changed this to return a new, subset array
+//          rather than alter the existing one
+// XXX: Fix the *valid pointer.
+psArray *pmPeaksSubset(
+    psArray *peaks,
+    psF32 maxValue,
+    const psRegion valid)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(peaks, NULL);
+
+    psArray *output = psArrayAlloc (200);
+    output->n = 0;
+
+    psTrace (".pmObjects.pmCullPeaks", 3, "list size is %d\n", peaks->n);
+
+    for (int i = 0; i < peaks->n; i++) {
+        pmPeak *tmpPeak = (pmPeak *) peaks->data[i];
+        if (tmpPeak->counts > maxValue)
+            continue;
+        if (isItInThisRegion(valid, tmpPeak->x, tmpPeak->y))
+            continue;
+        psArrayAdd (output, 200, tmpPeak);
+    }
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(output);
+}
Index: trunk/psModules/src/objects/pmPeaks.h
===================================================================
--- trunk/psModules/src/objects/pmPeaks.h	(revision 6872)
+++ trunk/psModules/src/objects/pmPeaks.h	(revision 6872)
@@ -0,0 +1,143 @@
+/** @file  pmPeaks.h
+ *
+ * The process of finding, measuring, and classifying astronomical sources on
+ * images is one of the critical tasks of the IPP or any astronomical software
+ * system. This file will define structures and functions related to the task
+ * of source detection and measurement. The elements defined in this section
+ * are generally low-level components which can be connected together to
+ * construct a complete object measurement suite.
+ *
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_PEAKS_H
+# define PM_PEAKS_H
+
+/** pmPeakType
+ *
+ *  A peak pixel may have several features which may be determined when the
+ *  peak is found or measured. These are specified by the pmPeakType enum.
+ *  PM_PEAK_LONE represents a single pixel which is higher than its 8 immediate
+ *  neighbors.  The PM_PEAK_EDGE represents a peak pixel which touching the image
+ *  edge. The PM_PEAK_FLAT represents a peak pixel which has more than a specific
+ *  number of neighbors at the same value, within some tolarence:
+ *
+ */
+typedef enum {
+    PM_PEAK_LONE,                       ///< Isolated peak.
+    PM_PEAK_EDGE,                       ///< Peak on edge.
+    PM_PEAK_FLAT,                       ///< Peak has equal-value neighbors.
+    PM_PEAK_UNDEF                       ///< Undefined.
+} pmPeakType;
+
+
+/** pmPeak data structure
+ *
+ *  A source has the capacity for several types of measurements. The
+ *  simplest measurement of a source is the location and flux of the peak pixel
+ *  associated with the source:
+ *
+ */
+typedef struct
+{
+    int x;                              ///< X-coordinate of peak pixel.
+    int y;                              ///< Y-coordinate of peak pixel.
+    float counts;                       ///< Value of peak pixel (above sky?).
+    pmPeakType type;                   ///< Description of peak.
+}
+pmPeak;
+
+
+/** pmPeakAlloc()
+ *
+ *  @return pmPeak*    newly allocated pmPeak with all internal pointers set to NULL
+ */
+pmPeak *pmPeakAlloc(
+    int x,    ///< Row-coordinate in image space
+    int y,    ///< Col-coordinate in image space
+    float counts,   ///< The value of the peak pixel
+    pmPeakType type   ///< The type of peak pixel
+);
+
+
+/** pmFindVectorPeaks()
+ *
+ * Find all local peaks in the given vector above the given threshold. A peak
+ * is defined as any element with a value greater than its two neighbors and with
+ * a value above the threshold. Two types of special cases must be addressed.
+ * Equal value elements: If an element has the same value as the following
+ * element, it is not considered a peak. If an element has the same value as the
+ * preceding element (but not the following), then it is considered a peak. Note
+ * that this rule (arbitrarily) identifies flat regions by their trailing edge.
+ * Edge cases: At start of the vector, the element must be higher than its
+ * neighbor. At the end of the vector, the element must be higher or equal to its
+ * neighbor. These two rules again places the peak associated with a flat region
+ * which touches the image edge at the image edge. The result of this function is
+ * a vector containing the coordinates (element number) of the detected peaks
+ * (type psU32).
+ *
+ */
+psVector *pmFindVectorPeaks(
+    const psVector *vector,  ///< The input vector (float)
+    float threshold   ///< Threshold above which to find a peak
+);
+
+
+/** pmFindImagePeaks()
+ *
+ * Find all local peaks in the given image above the given threshold. This
+ * function should find all row peaks using pmFindVectorPeaks, then test each row
+ * peak and exclude peaks which are not local peaks. A peak is a local peak if it
+ * has a higher value than all 8 neighbors. If the peak has the same value as its
+ * +y neighbor or +x neighbor, it is NOT a local peak. If any other neighbors
+ * have an equal value, the peak is considered a valid peak. Note two points:
+ * first, the +x neighbor condition is already enforced by pmFindVectorPeaks.
+ * Second, these rules have the effect of making flat-topped regions have single
+ * peaks at the (+x,+y) corner. When selecting the peaks, their type must also be
+ * set. The result of this function is an array of pmPeak entries.
+ *
+ */
+psArray *pmFindImagePeaks(
+    const psImage *image,  ///< The input image where peaks will be found (float)
+    float threshold   ///< Threshold above which to find a peak
+);
+
+
+/** pmCullPeaks()
+ *
+ * Eliminate peaks from the psList that have a peak value above the given
+ * maximum, or fall outside the valid region.
+ *
+ */
+psList *pmCullPeaks(
+    psList *peaks,   ///< The psList of peaks to be culled
+    float maxValue,   ///< Cull peaks above this value
+    const psRegion valid                ///< Cull peaks otside this psRegion
+);
+
+
+/** pmPeaksSubset()
+ *
+ * Create a new peaks array, removing certain types of peaks from the input
+ * array of peaks based on the given criteria. Peaks should be eliminated if they
+ * have a peak value above the given maximum value limit or if the fall outside
+ * the valid region.  The result of the function is a new array with a reduced
+ * number of peaks.
+ *
+ */
+psArray *pmPeaksSubset(
+    psArray *peaks,                     ///< Add comment.
+    float maxvalue,                     ///< Add comment.
+    const psRegion valid                ///< Add comment.
+);
+
+int pmPeaksCompareAscend (const void **a, const void **b);
+int pmPeaksCompareDescend (const void **a, const void **b);
+
+# endif /* PM_PEAKS_H */
Index: trunk/psModules/src/objects/pmSource.c
===================================================================
--- trunk/psModules/src/objects/pmSource.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSource.c	(revision 6872)
@@ -0,0 +1,609 @@
+/** @file  pmSource.c
+ *
+ *  Functions to define and manipulate sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModel.h"
+#include "pmSource.h"
+
+static void sourceFree(pmSource *tmp)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    psFree(tmp->peak);
+    psFree(tmp->pixels);
+    psFree(tmp->weight);
+    psFree(tmp->mask);
+    psFree(tmp->moments);
+    psFree(tmp->modelPSF);
+    psFree(tmp->modelEXT);
+    psFree(tmp->blends);
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+}
+
+/******************************************************************************
+pmSourceAlloc(): Allocate the pmSource structure and initialize its members
+to NULL.
+*****************************************************************************/
+pmSource *pmSourceAlloc()
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    pmSource *tmp = (pmSource *) psAlloc(sizeof(pmSource));
+    tmp->peak = NULL;
+    tmp->pixels = NULL;
+    tmp->weight = NULL;
+    tmp->mask = NULL;
+    tmp->moments = NULL;
+    tmp->blends = NULL;
+    tmp->modelPSF = NULL;
+    tmp->modelEXT = NULL;
+    tmp->type = PM_SOURCE_TYPE_UNKNOWN;
+    tmp->mode = PM_SOURCE_MODE_DEFAULT;
+    psMemSetDeallocator(tmp, (psFreeFunc) sourceFree);
+
+    tmp->psfMag = 32.0;
+    tmp->extMag = 32.0;
+    tmp->errMag = 32.0;
+    tmp->apMag  = 32.0;
+    tmp->pixWeight = 0.0;
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmp);
+}
+
+// x,y are defined in the parent image coords of readout->image
+bool pmSourceDefinePixels(pmSource *mySource,
+                          const pmReadout *readout,
+                          psF32 x,
+                          psF32 y,
+                          psF32 Radius)
+{
+    psRegion srcRegion;
+
+    // Grab a subimage of the original image of size (2 * outerRadius).
+    srcRegion = psRegionForSquare (x, y, Radius);
+    srcRegion = psRegionForImage (readout->image, srcRegion);
+
+    mySource->pixels = psImageSubset(readout->image, srcRegion);
+    mySource->weight = psImageSubset(readout->weight, srcRegion);
+    mySource->mask   = psImageSubset(readout->mask,  srcRegion);
+    mySource->region = srcRegion;
+
+    return true;
+}
+
+bool pmSourceRedefinePixels(pmSource *mySource,
+                            const pmReadout *readout,
+                            psF32 x,
+                            psF32 y,
+                            psF32 Radius)
+{
+    bool extend;
+    psRegion newRegion;
+
+    if (Radius == 0)
+        return false;
+
+    // check to see if new region is completely contained within old region
+    newRegion = psRegionForSquare (x, y, Radius);
+    newRegion = psRegionForImage (readout->image, newRegion);
+
+    extend = false;
+    extend |= (int)(newRegion.x0) < (int)(mySource->region.x0);
+    extend |= (int)(newRegion.x1) > (int)(mySource->region.x1);
+    extend |= (int)(newRegion.y0) < (int)(mySource->region.y0);
+    extend |= (int)(newRegion.y1) > (int)(mySource->region.y1);
+
+    extend |= (mySource->pixels == NULL);
+    extend |= (mySource->weight == NULL);
+    extend |= (mySource->mask == NULL);
+
+    // extend = true;
+    if (extend) {
+        // Grab a new subimage
+        psFree (mySource->pixels);
+        psFree (mySource->weight);
+        psFree (mySource->mask);
+
+        mySource->pixels = psImageSubset(readout->image,  newRegion);
+        mySource->weight = psImageSubset(readout->weight, newRegion);
+        mySource->mask   = psImageSubset(readout->mask,   newRegion);
+        mySource->region = newRegion;
+    }
+
+    return extend;
+}
+
+/******************************************************************************
+    pmSourcePSFClump(source, metadata): Find the likely PSF clump in the 
+    sigma-x, sigma-y plane. return 0,0 clump in case of error. 
+*****************************************************************************/
+
+// XXX EAM include a S/N cutoff in selecting the sources?
+pmPSFClump pmSourcePSFClump(psArray *sources, psMetadata *metadata)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+
+    # define NPIX 10
+    # define SCALE 0.1
+
+    psArray *peaks  = NULL;
+    pmPSFClump emptyClump = {0.0, 0.0, 0.0, 0.0};
+    pmPSFClump psfClump = emptyClump;
+
+    PS_ASSERT_PTR_NON_NULL(sources, emptyClump);
+    PS_ASSERT_PTR_NON_NULL(metadata, emptyClump);
+
+    // find the sigmaX, sigmaY clump
+    {
+        psStats *stats  = NULL;
+        psImage *splane = NULL;
+        int binX, binY;
+        bool status;
+
+        psF32 SX_MAX = psMetadataLookupF32 (&status, metadata, "MOMENTS_SX_MAX");
+        if (!status)
+            SX_MAX = 10.0;
+        psF32 SY_MAX = psMetadataLookupF32 (&status, metadata, "MOMENTS_SY_MAX");
+        if (!status)
+            SY_MAX = 10.0;
+
+        // construct a sigma-plane image
+        // psImageAlloc does zero the data
+        splane = psImageAlloc (SX_MAX/SCALE, SY_MAX/SCALE, PS_TYPE_F32);
+        for (int i = 0; i < splane->numRows; i++)
+        {
+            memset (splane->data.F32[i], 0, splane->numCols*sizeof(PS_TYPE_F32));
+        }
+
+        // place the sources in the sigma-plane image (ignore 0,0 values?)
+        for (psS32 i = 0 ; i < sources->n ; i++)
+        {
+            pmSource *tmpSrc = (pmSource *) sources->data[i];
+            if (tmpSrc == NULL) {
+                continue;
+            }
+            if (tmpSrc->moments == NULL) {
+                continue;
+            }
+
+            // Sx,Sy are limited at 0.  a peak at 0,0 is artificial
+            if ((fabs(tmpSrc->moments->Sx) < FLT_EPSILON) && (fabs(tmpSrc->moments->Sy) < FLT_EPSILON)) {
+                continue;
+            }
+
+            // for the moment, force splane dimensions to be 10x10 image pix
+            binX = tmpSrc->moments->Sx/SCALE;
+            if (binX < 0)
+                continue;
+            if (binX >= splane->numCols)
+                continue;
+
+            binY = tmpSrc->moments->Sy/SCALE;
+            if (binY < 0)
+                continue;
+            if (binY >= splane->numRows)
+                continue;
+
+            splane->data.F32[binY][binX] += 1.0;
+        }
+
+        // find the peak in this image
+        stats = psStatsAlloc (PS_STAT_MAX);
+        stats = psImageStats (stats, splane, NULL, 0);
+        peaks = pmFindImagePeaks (splane, stats[0].max / 2);
+        psTrace (".pmObjects.pmSourceRoughClass", 2, "clump threshold is %f\n", stats[0].max/2);
+
+        psFree (splane);
+        psFree (stats);
+
+    }
+    // XXX EAM : possible errors:
+    //           1) no peak in splane
+    //           2) no significant peak in splane
+
+    // measure statistics on Sx, Sy if Sx, Sy within range of clump
+    {
+        pmPeak *clump;
+        psF32 minSx, maxSx;
+        psF32 minSy, maxSy;
+        psVector *tmpSx = NULL;
+        psVector *tmpSy = NULL;
+        psStats *stats  = NULL;
+
+        // XXX EAM : this lets us takes the single highest peak
+        psArraySort (peaks, pmPeaksCompareDescend);
+        clump = peaks->data[0];
+        psTrace (".pmObjects.pmSourceRoughClass", 2, "clump is at %d, %d (%f)\n", clump->x, clump->y, clump->counts);
+
+        // define section window for clump
+        minSx = clump->x * SCALE - 0.2;
+        maxSx = clump->x * SCALE + 0.2;
+        minSy = clump->y * SCALE - 0.2;
+        maxSy = clump->y * SCALE + 0.2;
+
+        tmpSx = psVectorAlloc (sources->n, PS_TYPE_F32);
+        tmpSy = psVectorAlloc (sources->n, PS_TYPE_F32);
+        tmpSx->n = 0;
+        tmpSy->n = 0;
+
+        // XXX clip sources based on flux?
+        // create vectors with Sx, Sy values in window
+        for (psS32 i = 0 ; i < sources->n ; i++)
+        {
+            pmSource *tmpSrc = (pmSource *) sources->data[i];
+
+            if (tmpSrc->moments->Sx < minSx)
+                continue;
+            if (tmpSrc->moments->Sx > maxSx)
+                continue;
+            if (tmpSrc->moments->Sy < minSy)
+                continue;
+            if (tmpSrc->moments->Sy > maxSy)
+                continue;
+            tmpSx->data.F32[tmpSx->n] = tmpSrc->moments->Sx;
+            tmpSy->data.F32[tmpSy->n] = tmpSrc->moments->Sy;
+            tmpSx->n++;
+            tmpSy->n++;
+            if (tmpSx->n == tmpSx->nalloc) {
+                psVectorRealloc (tmpSx, tmpSx->nalloc + 100);
+                psVectorRealloc (tmpSy, tmpSy->nalloc + 100);
+            }
+        }
+
+        // measures stats of Sx, Sy
+        stats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+
+        stats = psVectorStats (stats, tmpSx, NULL, NULL, 0);
+        psfClump.X  = stats->clippedMean;
+        psfClump.dX = stats->clippedStdev;
+
+        stats = psVectorStats (stats, tmpSy, NULL, NULL, 0);
+        psfClump.Y  = stats->clippedMean;
+        psfClump.dY = stats->clippedStdev;
+
+        psTrace (".pmObjects.pmSourceRoughClass", 2, "clump  X,  Y: %f, %f\n", psfClump.X, psfClump.Y);
+        psTrace (".pmObjects.pmSourceRoughClass", 2, "clump DX, DY: %f, %f\n", psfClump.dX, psfClump.dY);
+        // these values should be pushed on the metadata somewhere
+
+        psFree (stats);
+        psFree (peaks);
+        psFree (tmpSx);
+        psFree (tmpSy);
+    }
+
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return (psfClump);
+}
+
+/******************************************************************************
+    pmSourceRoughClass(source, metadata): make a guess at the source
+    classification.
+     
+    XXX: push the clump info into the metadata?
+     
+    XXX: How can this function ever return FALSE?
+     
+    EAM: I moved S/N calculation to pmSourceMoments, using weight image
+*****************************************************************************/
+
+bool pmSourceRoughClass(psArray *sources, psMetadata *metadata, pmPSFClump clump)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+
+    psBool rc = true;
+
+    int Nsat     = 0;
+    int Next     = 0;
+    int Nstar    = 0;
+    int Npsf     = 0;
+    int Ncr      = 0;
+    int Nsatstar = 0;
+    // psRegion allArray = psRegionSet (0, 0, 0, 0);
+    psRegion inner;
+
+    // report stats on S/N values for star-like objects
+    psVector *starsn = psVectorAlloc (sources->n, PS_TYPE_F32);
+    starsn->n = 0;
+
+    // check return status value (do these exist?)
+    bool status;
+    psF32 PSF_SN_LIM = psMetadataLookupF32 (&status, metadata, "PSF_SN_LIM");
+
+    // XXX allow clump size to be scaled relative to sigmas?
+    // make rough IDs based on clumpX,Y,DX,DY
+    for (psS32 i = 0 ; i < sources->n ; i++) {
+
+        pmSource *tmpSrc = (pmSource *) sources->data[i];
+
+        tmpSrc->peak->type = 0;
+
+        psF32 sigX = tmpSrc->moments->Sx;
+        psF32 sigY = tmpSrc->moments->Sy;
+
+        // XXX EAM : can we use the value of SATURATE if mask is NULL?
+        // inner = psRegionForSquare (tmpSrc->peak->x - tmpSrc->mask->col0, tmpSrc->peak->y - tmpSrc->mask->row0, 2);
+        inner = psRegionForSquare (tmpSrc->peak->x, tmpSrc->peak->y, 2);
+        int Nsatpix = psImageCountPixelMask (tmpSrc->mask, inner, PM_SOURCE_MASK_SATURATED);
+
+        // saturated star (size consistent with PSF or larger)
+        // Nsigma should be user-configured parameter
+        bool big = (sigX > (clump.X - clump.dX)) && (sigY > (clump.Y - clump.dY));
+        big = true;
+        if ((Nsatpix > 1) && big) {
+            tmpSrc->type = PM_SOURCE_TYPE_STAR;
+            tmpSrc->mode = PM_SOURCE_MODE_SATSTAR;
+            Nsatstar ++;
+            continue;
+        }
+
+        // saturated object (not a star, eg bleed trails, hot pixels)
+        if (Nsatpix > 1) {
+            tmpSrc->type = PM_SOURCE_TYPE_SATURATED;
+            tmpSrc->mode = PM_SOURCE_MODE_DEFAULT;
+            Nsat ++;
+            continue;
+        }
+
+        // likely defect (too small to be stellar) (push out to 3 sigma)
+        // low S/N objects which are small are probably stellar
+        // only set candidate defects if
+        if ((sigX < 0.05) || (sigY < 0.05)) {
+            tmpSrc->type = PM_SOURCE_TYPE_DEFECT;
+            tmpSrc->mode = PM_SOURCE_MODE_DEFAULT;
+            Ncr ++;
+            continue;
+        }
+
+        // likely unsaturated extended source (too large to be stellar)
+        if ((sigX > (clump.X + 3*clump.dX)) || (sigY > (clump.Y + 3*clump.dY))) {
+            tmpSrc->type = PM_SOURCE_TYPE_EXTENDED;
+            tmpSrc->mode = PM_SOURCE_MODE_DEFAULT;
+            Next ++;
+            continue;
+        }
+
+        // the rest are probable stellar objects
+        starsn->data.F32[starsn->n] = tmpSrc->moments->SN;
+        starsn->n ++;
+        Nstar ++;
+
+        // PSF star (within 1.5 sigma of clump center, S/N > limit)
+        psF32 radius = hypot ((sigX-clump.X)/clump.dX, (sigY-clump.Y)/clump.dY);
+        if ((tmpSrc->moments->SN > PSF_SN_LIM) && (radius < 1.5)) {
+            tmpSrc->type = PM_SOURCE_TYPE_STAR;
+            tmpSrc->mode = PM_SOURCE_MODE_PSFSTAR;
+            Npsf ++;
+            continue;
+        }
+
+        // random type of star
+        tmpSrc->type = PM_SOURCE_TYPE_STAR;
+        tmpSrc->mode = PM_SOURCE_MODE_DEFAULT;
+    }
+
+    {
+        psStats *stats  = NULL;
+        stats = psStatsAlloc (PS_STAT_MIN | PS_STAT_MAX);
+        stats = psVectorStats (stats, starsn, NULL, NULL, 0);
+        psLogMsg ("pmObjects", 3, "SN range: %f - %f\n", stats[0].min, stats[0].max);
+        psFree (stats);
+        psFree (starsn);
+    }
+
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Nstar:    %3d\n", Nstar);
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Npsf:     %3d\n", Npsf);
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Next:     %3d\n", Next);
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Nsatstar: %3d\n", Nsatstar);
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Nsat:     %3d\n", Nsat);
+    psTrace (".pmObjects.pmSourceRoughClass", 2, "Ncr:      %3d\n", Ncr);
+
+    psTrace(__func__, 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/******************************************************************************
+pmSourceMoments(source, radius): this function takes a subImage defined in the
+pmSource data structure, along with the peak location, and determines the
+various moments associated with that peak.
+ 
+Requires the following to have been created:
+    pmSource
+    pmSource->peak
+    pmSource->pixels
+    pmSource->weight
+    pmSource->mask
+ 
+XXX: The peak calculations are done in image coords, not subImage coords.
+ 
+XXX EAM : this version clips input pixels on S/N
+XXX EAM : this version returns false for several reasons
+*****************************************************************************/
+# define VALID_RADIUS(X,Y,RAD2) (((RAD2) >= (PS_SQR(X) + PS_SQR(Y))) ? 1 : 0)
+
+bool pmSourceMoments(pmSource *source,
+                     psF32 radius)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->mask, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(radius, 0.0, false);
+
+    //
+    // XXX: Verify the setting for sky if source->moments == NULL.
+    //
+    psF32 sky = 0.0;
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    } else {
+        sky = source->moments->Sky;
+    }
+
+    //
+    // Sum = SUM (z - sky)
+    // X1  = SUM (x - xc)*(z - sky)
+    // X2  = SUM (x - xc)^2 * (z - sky)
+    // XY  = SUM (x - xc)*(y - yc)*(z - sky)
+    //
+    psF32 peakPixel = -PS_MAX_F32;
+    psS32 numPixels = 0;
+    psF32 Sum = 0.0;
+    psF32 Var = 0.0;
+    psF32 X1 = 0.0;
+    psF32 Y1 = 0.0;
+    psF32 X2 = 0.0;
+    psF32 Y2 = 0.0;
+    psF32 XY = 0.0;
+    psF32 x  = 0;
+    psF32 y  = 0;
+    psF32 R2 = PS_SQR(radius);
+
+    psF32 xPeak = source->peak->x;
+    psF32 yPeak = source->peak->y;
+    psF32 xOff = source->pixels->col0 - source->peak->x;
+    psF32 yOff = source->pixels->row0 - source->peak->y;
+
+    // XXX why do I get different results for these two methods of finding Sx?
+    // XXX Sx, Sy would be better measured if we clip pixels close to sky
+    // XXX Sx, Sy can still be imaginary, so we probably need to keep Sx^2?
+    // We loop through all pixels in this subimage (source->pixels), and for each
+    // pixel that is not masked, AND within the radius of the peak pixel, we
+    // proceed with the moments calculation.  need to do two loops for a
+    // numerically stable result.  first loop: get the sums.
+    // XXX EAM : mask == 0 is valid
+
+    for (psS32 row = 0; row < source->pixels->numRows ; row++) {
+
+        psF32 *vPix = source->pixels->data.F32[row];
+        psF32 *vWgt = source->weight->data.F32[row];
+        psU8  *vMsk = (source->mask == NULL) ? NULL : source->mask->data.U8[row];
+
+        for (psS32 col = 0; col < source->pixels->numCols ; col++, vPix++, vWgt++) {
+            if ((vMsk != NULL) && *vMsk) {
+                vMsk++;
+                continue;
+            }
+
+            psF32 xDiff = col + xOff;
+            psF32 yDiff = row + yOff;
+
+            // radius is just a function of (xDiff, yDiff)
+            if (!VALID_RADIUS(xDiff, yDiff, R2)) {
+                if (vMsk != NULL)
+                    vMsk++;
+                continue;
+            }
+
+            psF32 pDiff = *vPix - sky;
+            psF32 wDiff = *vWgt;
+
+            // XXX EAM : check for valid S/N in pixel
+            // XXX EAM : should this limit be user-defined?
+            if (PS_SQR(pDiff) < wDiff) {
+                if (vMsk != NULL)
+                    vMsk++;
+                continue;
+            }
+
+            Var += wDiff;
+            Sum += pDiff;
+
+            psF32 xWght = xDiff * pDiff;
+            psF32 yWght = yDiff * pDiff;
+
+            X1  += xWght;
+            Y1  += yWght;
+
+            XY  += xDiff * yWght;
+            X2  += xDiff * xWght;
+            Y2  += yDiff * yWght;
+
+            peakPixel = PS_MAX (*vPix, peakPixel);
+            numPixels++;
+            if (vMsk != NULL)
+                vMsk++;
+        }
+    }
+
+    // if we have less than (1/4) of the possible pixels, force a retry
+    // XXX EAM - the limit is a bit arbitrary.  make it user defined?
+    if ((numPixels < 0.75*R2) || (Sum <= 0)) {
+        psTrace (".psModules.pmSourceMoments", 3, "no valid pixels for source\n");
+        psTrace(__func__, 3, "---- %s(false) end ----\n", __func__);
+        return (false);
+    }
+
+    psTrace (".psModules.pmSourceMoments", 5,
+             "sky: %f  Sum: %f  X1: %f  Y1: %f  X2: %f  Y2: %f  XY: %f  Npix: %d\n",
+             sky, Sum, X1, Y1, X2, Y2, XY, numPixels);
+
+    //
+    // first moment X  = X1/Sum + xc
+    // second moment X = sqrt (X2/Sum - (X1/Sum)^2)
+    // Sxy             = XY / Sum
+    //
+    x = X1/Sum;
+    y = Y1/Sum;
+    if ((fabs(x) > radius) || (fabs(y) > radius)) {
+        psTrace (".psModules.pmSourceMoments", 3,
+                 "large centroid swing; invalid peak %d, %d\n",
+                 source->peak->x, source->peak->y);
+        psTrace(__func__, 3, "---- %s(false) end ----\n", __func__);
+        return (false);
+    }
+
+    source->moments->x = x + xPeak;
+    source->moments->y = y + yPeak;
+
+    // XXX EAM : Sxy needs to have x*y subtracted
+    source->moments->Sxy = XY/Sum - x*y;
+    source->moments->Sum = Sum;
+    source->moments->SN  = Sum / sqrt(Var);
+    source->moments->Peak = peakPixel;
+    source->moments->nPixels = numPixels;
+
+    // XXX EAM : these values can be negative, so we need to limit the range
+    source->moments->Sx = sqrt(PS_MAX(X2/Sum - PS_SQR(x), 0));
+    source->moments->Sy = sqrt(PS_MAX(Y2/Sum - PS_SQR(y), 0));
+
+    psTrace (".psModules.pmSourceMoments", 4,
+             "sky: %f  Sum: %f  x: %f  y: %f  Sx: %f  Sy: %f  Sxy: %f\n",
+             sky, Sum, source->moments->x, source->moments->y,
+             source->moments->Sx, source->moments->Sy, source->moments->Sxy);
+
+    psTrace(__func__, 3, "---- %s(true) end ----\n", __func__);
+    return(true);
+}
+
+pmModel *pmSourceSelectModel (pmSource *source)
+{
+    switch (source->type) {
+    case PM_SOURCE_TYPE_STAR:
+        return source->modelPSF;
+
+    case PM_SOURCE_TYPE_EXTENDED:
+        return source->modelEXT;
+
+    default:
+        return NULL;
+    }
+    return NULL;
+}
Index: trunk/psModules/src/objects/pmSource.h
===================================================================
--- trunk/psModules/src/objects/pmSource.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSource.h	(revision 6872)
@@ -0,0 +1,216 @@
+/** @file  pmSource.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_H
+# define PM_SOURCE_H
+
+/**
+ * In the object analysis process, we will use specific mask values to mark the
+ * image pixels. The following structure defines the relevant mask values.
+ *
+ * XXX: This is probably a bad solution: we will want to set mask values
+ * outside of the PSPHOT code.  Perhaps we can set up a registered set of mask
+ * values with specific meanings that other functions can add to or define?
+ */
+typedef enum {
+    PM_SOURCE_MASK_CLEAR     = 0x00,
+    PM_SOURCE_MASK_INVALID   = 0x01,
+    PM_SOURCE_MASK_SATURATED = 0x02,
+    PM_SOURCE_MASK_MARKED    = 0x08,
+} psphotMaskValues;
+
+/** pmSourceType enumeration
+ *
+ * A given source may be identified as most-likely to be one of several source
+ * types. The pmSource entry pmSourceType defines the current best-guess for this
+ * source.
+ *
+ * XXX: The values given below are currently illustrative and will require
+ * some modification as the source classification code is developed. (TBD)
+ *
+ */
+typedef enum {
+    PM_SOURCE_TYPE_UNKNOWN,  ///< a cosmic-ray
+    PM_SOURCE_TYPE_DEFECT,  ///< a cosmic-ray
+    PM_SOURCE_TYPE_SATURATED,  ///< random saturated pixels
+    PM_SOURCE_TYPE_STAR,  ///< a good-quality star
+    PM_SOURCE_TYPE_EXTENDED,  ///< an extended object (eg, galaxy)
+} pmSourceType;
+
+typedef enum {
+    PM_SOURCE_MODE_DEFAULT    = 0x0000, ///<
+    PM_SOURCE_MODE_PSFMODEL   = 0x0001, ///<
+    PM_SOURCE_MODE_EXTMODEL   = 0x0002, ///<
+    PM_SOURCE_MODE_SUBTRACTED = 0x0004, ///<
+    PM_SOURCE_MODE_FITTED     = 0x0008, ///<
+    PM_SOURCE_MODE_FAIL       = 0x0010, ///<
+    PM_SOURCE_MODE_POOR       = 0x0020, ///<
+    PM_SOURCE_MODE_PAIR       = 0x0040, ///<
+    PM_SOURCE_MODE_PSFSTAR    = 0x0080, ///<
+    PM_SOURCE_MODE_SATSTAR    = 0x0100, ///<
+    PM_SOURCE_MODE_BLEND      = 0x0200, ///<
+    PM_SOURCE_MODE_LINEAR     = 0x0400, ///<
+    PM_SOURCE_MODE_TEMPSUB    = 0x0800, ///< XXX get me a better name!
+} pmSourceMode;
+
+/** pmSource data structure
+ *
+ *  This source has the capacity for several types of measurements. The
+ *  simplest measurement of a source is the location and flux of the peak pixel
+ *  associated with the source:
+ *
+ */
+typedef struct
+{
+    pmPeak *peak;   ///< Description of peak pixel.
+    psImage *pixels;   ///< Rectangular region including object pixels.
+    psImage *weight;   ///< Image variance.
+    psImage *mask;   ///< Mask which marks pixels associated with objects.
+    pmMoments *moments;   ///< Basic moments measure for the object.
+    pmModel *modelPSF;   ///< PSF Model fit (parameters and type)
+    pmModel *modelEXT;   ///< EXT (floating) Model fit (parameters and type).
+    pmSourceType type;   ///< Best identification of object.
+    pmSourceMode mode;   ///< Best identification of object.
+    psArray *blends;
+    float psfMag;
+    float extMag;
+    float errMag;
+    float apMag;
+    float pixWeight;   // model-weighted coverage of valid pixels
+    psRegion region;   // area on image covered by selected pixels
+}
+pmSource;
+
+/** pmPSFClump data structure
+ *
+ * A collection of object moment measurements can be used to determine
+ * approximate object classes. The key to this analysis is the location and
+ * statistics (in the second-moment plane,
+ *
+ */
+typedef struct
+{
+    float X;
+    float dX;
+    float Y;
+    float dY;
+}
+pmPSFClump;
+
+
+/** pmSourceAlloc()
+ *
+ */
+pmSource  *pmSourceAlloc();
+
+
+/** pmSourceDefinePixels()
+ *
+ * Define psImage subarrays for the source located at coordinates x,y on the
+ * image set defined by readout. The pixels defined by this operation consist of
+ * a square window (of full width 2Radius+1) centered on the pixel which contains
+ * the given coordinate, in the frame of the readout. The window is defined to
+ * have limits which are valid within the boundary of the readout image, thus if
+ * the radius would fall outside the image pixels, the subimage is truncated to
+ * only consist of valid pixels. If readout->mask or readout->weight are not
+ * NULL, matching subimages are defined for those images as well. This function
+ * fails if no valid pixels can be defined (x or y less than Radius, for
+ * example). This function should be used to define a region of interest around a
+ * source, including both source and sky pixels.
+ *
+ */
+bool pmSourceDefinePixels(
+    pmSource *mySource,                 ///< source to be re-defined
+    const pmReadout *readout,  ///< base the source on this readout
+    psF32 x,                            ///< center coords of source
+    psF32 y,                            ///< center coords of source
+    psF32 Radius                        ///< size of box on source
+);
+
+bool pmSourceRedefinePixels (
+    pmSource *mySource,   ///< source to be re-defined
+    const pmReadout *readout,   ///< base the source on this readout
+    psF32 x,     ///< center coords of source
+    psF32 y,      ///< center coords of source
+    psF32 Radius   ///< size of box on source
+);
+
+/** pmSourcePSFClump()
+ *
+ * We use the source moments to make an initial, approximate source
+ * classification, and as part of the information needed to build a PSF model for
+ * the image. As long as the PSF shape does not vary excessively across the
+ * image, the sources which are represented by a PSF (the start) will have very
+ * similar second moments. The function pmSourcePSFClump searches a collection of
+ * sources with measured moments for a group with moments which are all very
+ * similar. The function returns a pmPSFClump structure, representing the
+ * centroid and size of the clump in the sigma_x, sigma_y second-moment plane.
+ *
+ * The goal is to identify and characterize the stellar clump within the
+ * sigma_x, sigma_y second-moment plane.  To do this, an image is constructed to
+ * represent this plane.  The units of sigma_x and sigma_y are in image pixels. A
+ * pixel in this analysis image represents 0.1 pixels in the input image. The
+ * dimensions of the image need only be 10 pixels. The peak pixel in this image
+ * (above a threshold of half of the image maximum) is found. The coordinates of
+ * this peak pixel represent the 2D mode of the sigma_x, sigma_y distribution.
+ * The sources with sigma_x, sigma_y within 0.2 pixels of this value are then
+ *  * used to calculate the median and standard deviation of the sigma_x, sigma_y
+ * values. These resulting values are returned via the pmPSFClump structure.
+ *
+ * The return value indicates the success (TRUE) of the operation.
+ *
+ * XXX: Limit the S/N of the candidate sources (part of Metadata)? (TBD).
+ * XXX: Save the clump parameters on the Metadata (TBD)
+ *
+ */
+pmPSFClump pmSourcePSFClump(
+    psArray *source,   ///< The input pmSource
+    psMetadata *metadata  ///< Contains classification parameters
+);
+
+
+/** pmSourceRoughClass()
+ *
+ * Based on the specified data values, make a guess at the source
+ * classification. The sources are provides as a psArray of pmSource entries.
+ * Definable parameters needed to make the classification are provided to the
+ * routine with the psMetadata structure. The rules (in SDRS) refer to values which
+ * can be extracted from the metadata using the given keywords. Except as noted,
+ * the data type for these parameters are psF32.
+ *
+ */
+bool pmSourceRoughClass(
+    psArray *source,   ///< The input pmSource
+    psMetadata *metadata,  ///< Contains classification parameters
+    pmPSFClump clump   ///< Statistics about the PSF clump
+);
+
+
+/** pmSourceMoments()
+ *
+ * Measure source moments for the given source, using the value of
+ * source.moments.sky provided as the local background value and the peak
+ * coordinates as the initial source location. The resulting moment values are
+ * applied to the source.moments entry, and the source is returned. The moments
+ * are measured within the given circular radius of the source.peak coordinates.
+ * The return value indicates the success (TRUE) of the operation.
+ *
+ */
+bool pmSourceMoments(
+    pmSource *source,   ///< The input pmSource for which moments will be computed
+    float radius   ///< Use a circle of pixels around the peak
+);
+
+
+// select the model used for this source
+pmModel *pmSourceSelectModel (pmSource *source);
+
+# endif /* PM_SOURCE_H */
Index: trunk/psModules/src/objects/pmSourceContour.c
===================================================================
--- trunk/psModules/src/objects/pmSourceContour.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceContour.c	(revision 6872)
@@ -0,0 +1,446 @@
+/** @file  pmSourceContour.c
+ *
+ *  Functions to measure the local sky and sky variance for sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceContour.h"
+
+/******************************************************************************
+findValue(source, level, row, col, dir): a private function which determines
+the column coordinate of the model function which has the value "level".  If
+dir equals 0, then you loop leftwards from the peak pixel, otherwise,
+rightwards.
+ 
+XXX: reverse order of row,col args?
+ 
+XXX: Input row/col are in image coords.
+ 
+XXX: The result is returned in image coords.
+*****************************************************************************/
+# define LEFT false
+# define RIGHT true
+
+// return the first coordinate at or below the threshold in the requested direction
+static int findContourNeg(
+    psImage *image,
+    float threshold,
+    int x,
+    int y,
+    bool right)
+{
+
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+
+    int incr;
+    int subCol;
+    int lastColumn;
+    if (right) {
+        incr = 1;
+        lastColumn = image->numCols - 1;
+    } else {
+        incr = -1;
+        lastColumn = 0;
+    }
+
+    subCol = x;
+
+    while (subCol != lastColumn) {
+        float value = image->data.F32[y][subCol];
+        if (value <= threshold) {
+            psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+    return (lastColumn);
+}
+
+// return the last coordinate at or below the threshold in the requested direction
+static int findContourPos(
+    psImage *image,
+    float threshold,
+    int x,
+    int y,
+    bool right,
+    int xEnd)
+{
+
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+
+    int incr;
+    int subCol;
+    int lastColumn;
+    if (right) {
+        incr = 1;
+        lastColumn = PS_MIN (image->numCols - 1, xEnd);
+    } else {
+        incr = -1;
+        lastColumn = PS_MAX (0, xEnd);
+    }
+
+    subCol = x;
+    while (subCol != lastColumn) {
+        float value = image->data.F32[y][subCol];
+        if (value >= threshold) {
+            psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+            if (subCol == x) {
+                return (subCol);
+            }
+            return (subCol);
+        }
+        subCol += incr;
+    }
+    psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+    return (lastColumn);
+}
+
+/******************************************************************************
+findValue(source, level, row, col, dir): a private function which determines
+the column coordinate of the model function which has the value "level".  If
+dir equals 0, then you loop leftwards from the peak pixel, otherwise,
+rightwards.
+ 
+XXX: reverse order of row,col args?
+ 
+XXX: Input row/col are in image coords.
+ 
+XXX: The result is returned in image coords.
+*****************************************************************************/
+static psF32 findValue(pmSource *source,
+                       psF32 level,
+                       psU32 row,
+                       psU32 col,
+                       psU32 dir)
+{
+    psTrace(__func__, 4, "---- %s() begin ----\n", __func__);
+    //
+    // Convert coords to subImage space.
+    //
+    psU32 subRow = row - source->pixels->row0;
+    psU32 subCol = col - source->pixels->col0;
+
+    // Ensure that the starting column is allowable.
+    if (!((0 <= subCol) && (subCol < source->pixels->numCols))) {
+        psError(PS_ERR_UNKNOWN, true, "Starting column outside subImage range");
+        psTrace(__func__, 4, "---- %s(NAN) end ----\n", __func__);
+        return(NAN);
+    }
+    if (!((0 <= subRow) && (subRow < source->pixels->numRows))) {
+        psTrace(__func__, 4, "---- %s(NAN) end ----\n", __func__);
+        psError(PS_ERR_UNKNOWN, true, "Starting row outside subImage range");
+        return(NAN);
+    }
+
+    // XXX EAM : i changed this to match pmModelEval above, but see
+    // XXX EAM   the note below in pmSourceContour
+    psF32 oldValue = pmModelEval(source->modelEXT, source->pixels, subCol, subRow);
+    if (oldValue == level) {
+        psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+        return(((psF32) (subCol + source->pixels->col0)));
+    }
+
+    //
+    // We define variables incr and lastColumn so that we can use the same loop
+    // whether we are stepping leftwards, or rightwards.
+    //
+    psS32 incr;
+    psS32 lastColumn;
+    if (dir == 0) {
+        incr = -1;
+        lastColumn = -1;
+    } else {
+        incr = 1;
+        lastColumn = source->pixels->numCols;
+    }
+    subCol+=incr;
+
+    while (subCol != lastColumn) {
+        psF32 newValue = pmModelEval(source->modelEXT, source->pixels, subCol, subRow);
+        if (oldValue == level) {
+            psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+            return((psF32) (subCol + source->pixels->col0));
+        }
+
+        if ((newValue <= level) && (level <= oldValue)) {
+            // This is simple linear interpolation.
+            psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+            return( ((psF32) (subCol + source->pixels->col0)) + ((psF32) incr) * ((level - newValue) / (oldValue - newValue)) );
+        }
+
+        if ((oldValue <= level) && (level <= newValue)) {
+            // This is simple linear interpolation.
+            psTrace(__func__, 4, "---- %s() end ----\n", __func__);
+            return( ((psF32) (subCol + source->pixels->col0)) + ((psF32) incr) * ((level - oldValue) / (newValue - oldValue)) );
+        }
+
+        subCol+=incr;
+    }
+
+    psTrace(__func__, 4, "---- %s(NAN) end ----\n", __func__);
+    return(NAN);
+}
+
+/******************************************************************************
+new implementation of source contour function
+*****************************************************************************/
+psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold)
+{
+
+    int xR, yR, x0, x1, x0s, x1s;
+
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+
+    int x = xc - image->col0;
+    int y = yc - image->row0;
+
+    // the requested point must be within the contour
+    if (image->data.F32[y][x] < threshold)
+        return NULL;
+
+    // Ensure that the starting column is allowable.
+    if (x < 0)
+        return NULL;
+    if (y < 0)
+        return NULL;
+    if (x >= image->numCols)
+        return NULL;
+    if (y >= image->numRows)
+        return NULL;
+
+    // Allocate data for x/y pairs.
+    psVector *xVec = psVectorAlloc(100, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc(100, PS_TYPE_F32);
+
+    // First row: find the left and right end-points
+    int Npt = 0;
+
+    x0 = findContourNeg (image, threshold, x, y, LEFT);
+    x1 = findContourNeg (image, threshold, x, y, RIGHT);
+    xVec->data.F32[Npt + 0] = image->col0 + x0;
+    xVec->data.F32[Npt + 1] = image->col0 + x1;
+    yVec->data.F32[Npt + 0] = image->row0 + y;
+    yVec->data.F32[Npt + 1] = image->row0 + y;
+    Npt += 2;
+
+    x0s = x0;
+    x1s = x1;
+
+    // look for contour outline above row
+    xR = x0s;
+    yR = y + 1;
+    while (yR < image->numRows) {
+        if (image->data.F32[yR][xR] < threshold) {
+            x0 = findContourPos (image, threshold, xR, yR, RIGHT, x1);
+            if (x0 == x1) {
+                // fprintf (stderr, "top: %d (%d - %d)\n", yR, xR, x1);
+                goto pt1;
+            }
+            x1 = findContourNeg (image, threshold, x0, yR, RIGHT);
+            x0--;
+        } else {
+            x0 = findContourNeg (image, threshold, xR, yR, LEFT);
+            x1 = findContourNeg (image, threshold, xR, yR, RIGHT);
+        }
+        // fprintf (stderr, "pos: %d (%d - %d)\n", yR, x0, x1);
+
+        xVec->data.F32[Npt + 0] = image->col0 + x0;
+        xVec->data.F32[Npt + 1] = image->col0 + x1;
+        yVec->data.F32[Npt + 0] = image->row0 + yR;
+        yVec->data.F32[Npt + 1] = image->row0 + yR;
+        Npt += 2;
+
+        if (Npt >= xVec->nalloc - 1) {
+            psVectorRealloc (xVec, xVec->nalloc + 100);
+            psVectorRealloc (yVec, yVec->nalloc + 100);
+        }
+        yR ++;
+        xR = x0;
+    }
+
+pt1:
+    // look for contour outline below row
+    xR = x0s;
+    x1 = x1s;
+    yR = y - 1;
+    while (yR >= 0) {
+        if (image->data.F32[yR][xR] < threshold) {
+            x0 = findContourPos (image, threshold, xR, yR, RIGHT, x1);
+            if (x0 == x1) {
+                // fprintf (stderr, "top: %d (%d - %d)\n", yR, xR, x1);
+                goto pt2;
+            }
+            x1 = findContourNeg (image, threshold, x0, yR, RIGHT);
+            x0--;
+        } else {
+            x0 = findContourNeg (image, threshold, xR, yR, LEFT);
+            x1 = findContourNeg (image, threshold, xR, yR, RIGHT);
+        }
+        // fprintf (stderr, "neg: %d (%d - %d)\n", yR, x0, x1);
+
+        xVec->data.F32[Npt + 0] = image->col0 + x0;
+        xVec->data.F32[Npt + 1] = image->col0 + x1;
+        yVec->data.F32[Npt + 0] = image->row0 + yR;
+        yVec->data.F32[Npt + 1] = image->row0 + yR;
+        Npt += 2;
+
+        if (Npt >= xVec->nalloc - 1) {
+            psVectorRealloc (xVec, xVec->nalloc + 100);
+            psVectorRealloc (yVec, yVec->nalloc + 100);
+        }
+        yR --;
+    }
+pt2:
+    xVec->n = Npt;
+    yVec->n = Npt;
+
+    // fprintf (stderr, "done\n");
+    psArray *tmpArray = psArrayAlloc(2);
+    tmpArray->data[0] = (psPtr *) xVec;
+    tmpArray->data[1] = (psPtr *) yVec;
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmpArray);
+}
+
+/******************************************************************************
+    pmSourceContour(src, img, level, mode): For an input subImage, and model, this
+    routine returns a psArray of coordinates that evaluate to the specified level.
+     
+    XXX: Probably should remove the "image" argument.
+    XXX: What type should the output coordinate vectors consist of?  col,row?
+    XXX: Why a pmArray output?
+    XXX: doex x,y correspond with col,row or row/col?
+    XXX: What is mode?
+    XXX: The top, bottom of the contour is not correctly determined.
+    XXX EAM : this function is using the model for the contour, but it should
+              be using only the image counts
+*****************************************************************************/
+psArray *pmSourceContour_Crude(pmSource *source,
+                               const psImage *image,
+                               psF32 level)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(image, false);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->modelEXT, false);
+    // XXX EAM : what is the purpose of modelPSF/modelEXT?
+
+    //
+    // Allocate data for x/y pairs.
+    //
+    psVector *xVec = psVectorAlloc(2 * source->pixels->numRows, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc(2 * source->pixels->numRows, PS_TYPE_F32);
+
+    //
+    // Start at the row with peak pixel, then decrement.
+    //
+    psS32 col = source->peak->x;
+    for (psS32 row = source->peak->y; row>= 0 ; row--) {
+        // XXX: yVec contain no real information.  Do we really need it?
+        yVec->data.F32[row] = (psF32) (source->pixels->row0 + row);
+        yVec->data.F32[row+yVec->n] = (psF32) (source->pixels->row0 + row);
+
+        // Starting at peak pixel, search leftwards for the column intercept.
+        psF32 leftIntercept = findValue(source, level, row, col, 0);
+        if (isnan(leftIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace(__func__, 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row] = ((psF32) source->pixels->col0) + leftIntercept;
+
+        // Starting at peak pixel, search rightwards for the column intercept.
+
+        psF32 rightIntercept = findValue(source, level, row, col, 1);
+        if (isnan(rightIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace(__func__, 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        psTrace(__func__, 4, "The intercepts are (%.2f, %.2f)\n", leftIntercept, rightIntercept);
+        xVec->data.F32[row+xVec->n] = ((psF32) source->pixels->col0) + rightIntercept;
+
+        // Set starting column for next row
+        col = (psS32) ((leftIntercept + rightIntercept) / 2.0);
+    }
+    //
+    // Start at the row (+1) with peak pixel, then increment.
+    //
+    col = source->peak->x;
+    for (psS32 row = 1 + source->peak->y; row < source->pixels->numRows ; row++) {
+        // XXX: yVec contain no real information.  Do we really need it?
+        yVec->data.F32[row] = (psF32) (source->pixels->row0 + row);
+        yVec->data.F32[row+yVec->n] = (psF32) (source->pixels->row0 + row);
+
+        // Starting at peak pixel, search leftwards for the column intercept.
+        psF32 leftIntercept = findValue(source, level, row, col, 0);
+        if (isnan(leftIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace(__func__, 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row] = ((psF32) source->pixels->col0) + leftIntercept;
+
+        // Starting at peak pixel, search rightwards for the column intercept.
+        psF32 rightIntercept = findValue(source, level, row, col, 1);
+        if (isnan(rightIntercept)) {
+            psError(PS_ERR_UNKNOWN, true, "Could not find contour edge (NAN)");
+            psFree(xVec);
+            psFree(yVec);
+            psTrace(__func__, 3, "---- %s(NULL) end ----\n", __func__);
+            return(NULL);
+            //psLogMsg(__func__, PS_LOG_WARN, "WARNING: Could not find contour edge (NAN)\n");
+        }
+        xVec->data.F32[row+xVec->n] = ((psF32) source->pixels->col0) + rightIntercept;
+
+        // Set starting column for next row
+        col = (psS32) ((leftIntercept + rightIntercept) / 2.0);
+    }
+
+    //
+    // Allocate an array for result, store coord vectors there.
+    //
+    psArray *tmpArray = psArrayAlloc(2);
+    tmpArray->data[0] = (psPtr *) yVec;
+    tmpArray->data[1] = (psPtr *) xVec;
+    psTrace(__func__, 3, "---- %s() end ----\n", __func__);
+    return(tmpArray);
+}
Index: trunk/psModules/src/objects/pmSourceContour.h
===================================================================
--- trunk/psModules/src/objects/pmSourceContour.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceContour.h	(revision 6872)
@@ -0,0 +1,37 @@
+/** @file  pmSourceContour.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_CONTOUR_H
+# define PM_SOURCE_CONTOUR_H
+
+psArray *pmSourceContour (psImage *image, int xc, int yc, float threshold);
+
+
+/** pmSourceContour()
+ *
+ * Find points in a contour for the given source at the given level. If type
+ * is PM_CONTOUR_CRUDE, the contour is found by starting at the source peak,
+ * running along each pixel row until the level is crossed, then interpolating to
+ * the level coordinate for that row. This is done for each row, with the
+ * starting point determined by the midpoint of the previous row, until the
+ * starting point has a value below the contour level. The returned contour
+ * consists of two vectors giving the x and y coordinates of the contour levels.
+ * This function may be used as part of the model guess inputs.  Other contour
+ * types may be specified in the future for more refined contours (TBD)
+ *
+ */
+psArray *pmSourceContour_Crude(
+    pmSource *source,   ///< The input pmSource
+    const psImage *image,  ///< The input image (float) (this arg should be removed)
+    float level   ///< The level of the contour
+);
+
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: trunk/psModules/src/objects/pmSourceFitModel.c
===================================================================
--- trunk/psModules/src/objects/pmSourceFitModel.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceFitModel.c	(revision 6872)
@@ -0,0 +1,559 @@
+/** @file  pmSourceFitModel.c
+ *
+ *  fit single source models to image pixels
+ *
+ *  @author EAM, IfA
+ *  @author GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceFitModel.h"
+
+// save as static values so they may be set externally
+static psF32 PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = 15;
+static psF32 PM_SOURCE_FIT_MODEL_TOLERANCE = 0.1;
+static bool PM_PSF_POISSON_WEIGHTS = true;
+
+bool pmSourceFitModelInit (float nIter, float tol, bool poissonErrors)
+{
+
+    PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = nIter;
+    PM_SOURCE_FIT_MODEL_TOLERANCE = tol;
+    PM_PSF_POISSON_WEIGHTS = poissonErrors;
+
+    return true;
+}
+
+bool pmSourceFitModel (pmSource *source,
+                       pmModel *model,
+                       pmSourceFitMode mode)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->mask, false);
+    PS_ASSERT_PTR_NON_NULL(source->weight, false);
+
+    // XXX EAM : is it necessary for the mask & weight to exist?  the
+    //           tests below could be conditions (!NULL)
+
+    psBool fitStatus = true;
+    psBool onPic     = true;
+    psBool rc        = true;
+
+    psVector *params = model->params;
+    psVector *dparams = model->dparams;
+    psVector *paramMask = NULL;
+
+    pmModelFunc modelFunc = pmModelFunc_GetFunction (model->type);
+
+    psF32 dSky = source->moments->dSky;
+
+    // maximum number of valid pixels
+    psS32 nPix = source->pixels->numRows * source->pixels->numCols;
+
+    // construct the coordinate and value entries
+    psArray *x = psArrayAlloc(nPix);
+    psVector *y = psVectorAlloc(nPix, PS_TYPE_F32);
+    psVector *yErr = psVectorAlloc(nPix, PS_TYPE_F32);
+
+    nPix = 0;
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->mask->data.U8[i][j]) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+            x->data[nPix] = (psPtr *) coord;
+            y->data.F32[nPix] = source->pixels->data.F32[i][j];
+            // psMinimizeLMChi2 takes wt = 1/dY^2
+            if (PM_PSF_POISSON_WEIGHTS) {
+                yErr->data.F32[nPix] = 1.0 / source->weight->data.F32[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / dSky;
+            }
+            // XXX EAM : suggestion from RHL is to use the local sky as weight
+            // to avoid the bias from systematic errors
+            // here we would just use the source sky variance
+            nPix++;
+        }
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+    // XXX EAM : the new minimization API supplies the constraints as a struct
+    // XXX the chisq if a fcn of source flux. the minimization criterion should
+    // probably  be scaled somehow by flux.  measure the trend?  it depends on
+    // the about of systematic error in the models themselves...
+    psMinimization *myMin = psMinimizationAlloc(PM_SOURCE_FIT_MODEL_NUM_ITERATIONS,
+                            PM_SOURCE_FIT_MODEL_TOLERANCE);
+    psMinConstrain *constrain = psMinConstrainAlloc();
+
+    // set parameter mask based on fitting mode
+    paramMask = psVectorAlloc (params->n, PS_TYPE_U8);
+    psVectorInit (paramMask, 1);
+
+    int nParams = 0;
+    switch (mode) {
+    case PM_SOURCE_FIT_NORM:
+        // NORM-only model fits only source normalization (Io)
+        nParams = 1;
+        paramMask->data.U8[1] = 0;
+        break;
+    case PM_SOURCE_FIT_PSF:
+        // PSF model only fits x,y,Io
+        nParams = 3;
+        paramMask->data.U8[1] = 0;
+        paramMask->data.U8[2] = 0;
+        paramMask->data.U8[3] = 0;
+        break;
+    case PM_SOURCE_FIT_EXT:
+        // EXT model fits all params (except sky)
+        nParams = params->n - 1;
+        psVectorInit (paramMask, 0);
+        paramMask->data.U8[0] = 1;
+        break;
+    case PM_SOURCE_FIT_PSF_AND_SKY:
+        nParams = 4;
+        psAbort ("pmSourceFitModel", "PSF + SKY not currently available");
+        break;
+    case PM_SOURCE_FIT_EXT_AND_SKY:
+        nParams = params->n;
+        psAbort ("pmSourceFitModel", "EXT + SKY not currently available");
+        break;
+    default:
+        psAbort ("pmSourceFitModel", "invalid fitting mode");
+    }
+    constrain->paramMask = paramMask;
+
+    if (nPix <  nParams + 1) {
+        psTrace (".pmObjects.pmSourceFitModel", 4, "insufficient valid pixels\n");
+        psTrace(__func__, 3, "---- %s(false) end ----\n", __func__);
+        model->status = PM_MODEL_BADARGS;
+        psFree (x);
+        psFree (y);
+        psFree (yErr);
+        psFree (myMin);
+        psFree (constrain);
+        psFree (paramMask);
+        return(false);
+    }
+
+    // Set the parameter range checks
+    pmModelLimits modelLimits = pmModelLimits_GetFunction (model->type);
+    modelLimits (&constrain->paramDelta, &constrain->paramMin, &constrain->paramMax);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F64);
+
+    psTrace (".pmObjects.pmSourceFitModel", 5, "fitting function\n");
+
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constrain, x, y, yErr, modelFunc);
+    for (int i = 0; i < dparams->n; i++) {
+        if ((paramMask != NULL) && paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F64[i][i]);
+    }
+
+    // save the resulting chisq, nDOF, nIter
+    model->chisq = myMin->value;
+    model->nIter = myMin->iter;
+    model->nDOF  = y->n - nParams;
+
+    // get the Gauss-Newton distance for fixed model parameters
+    if (paramMask != NULL) {
+        psVector *delta = psVectorAlloc (params->n, PS_TYPE_F64);
+        psMinimizeGaussNewtonDelta(delta, params, NULL, x, y, yErr, modelFunc);
+        for (int i = 0; i < dparams->n; i++) {
+            if (!paramMask->data.U8[i])
+                continue;
+            dparams->data.F32[i] = delta->data.F64[i];
+        }
+        psFree (delta);
+    }
+
+    // set the model success or failure status
+    if (!fitStatus) {
+        model->status = PM_MODEL_NONCONVERGE;
+    } else {
+        model->status = PM_MODEL_SUCCESS;
+    }
+
+    // models can go insane: reject these
+    onPic &= (params->data.F32[2] >= source->pixels->col0);
+    onPic &= (params->data.F32[2] <  source->pixels->col0 + source->pixels->numCols);
+    onPic &= (params->data.F32[3] >= source->pixels->row0);
+    onPic &= (params->data.F32[3] <  source->pixels->row0 + source->pixels->numRows);
+    if (!onPic) {
+        model->status = PM_MODEL_OFFIMAGE;
+    }
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+
+    psFree(x);
+    psFree(y);
+    psFree(yErr);
+    psFree(myMin);
+    psFree(covar);
+    psFree(constrain->paramMask);
+    psFree(constrain->paramMin);
+    psFree(constrain->paramMax);
+    psFree(constrain->paramDelta);
+    psFree(constrain);
+
+    rc = (onPic && fitStatus);
+    psTrace(__func__, 3, "---- %s(%d) end ----\n", __func__, rc);
+    return(rc);
+}
+
+/********************* Source Model Set Functions ***************************/
+
+// these static variables are used by one pass of pmSourceFitSet to store
+// data for a model set.  If we are going to make psphot thread-safe, these
+// will have to go in a structure of their own.
+// sky, p1.1, p1.2, p1.3,... p1.n, p2.1, p2.2,
+// nPar = nSrc*(nOnePar - 1) + 1
+static pmModelFunc mFunc;
+static int nPar;
+static psVector *onePar;
+static psVector *oneDeriv;
+
+bool pmModelFitSetInit (pmModelType type)
+{
+
+    mFunc = pmModelFunc_GetFunction (type);
+    nPar  = pmModelParameterCount (type);
+
+    onePar = psVectorAlloc (nPar, PS_DATA_F32);
+    oneDeriv = psVectorAlloc (nPar, PS_DATA_F32);
+
+    return true;
+}
+
+void pmModelFitSetClear (void)
+{
+
+    psFree (onePar);
+    psFree (oneDeriv);
+    return;
+}
+
+psF32 pmModelFitSet(psVector *deriv,
+                    const psVector *params,
+                    const psVector *x)
+{
+
+    psF32 value;
+    psF32 model;
+
+    psF32 *PAR = onePar->data.F32;
+    psF32 *dPAR = oneDeriv->data.F32;
+
+    psF32 *pars = params->data.F32;
+    psF32 *dpars = (deriv == NULL) ? NULL : deriv->data.F32;
+
+    int nSrc = (params->n - 1) / (nPar - 1);
+
+    PAR[0] = model = pars[0];
+    for (int i = 0; i < nSrc; i++) {
+        int nOff = i*nPar - i;
+        for (int n = 1; n < nPar; n++) {
+            PAR[n] = pars[nOff + n];
+        }
+        if (deriv == NULL) {
+            value = mFunc (NULL, onePar, x);
+        } else {
+            value = mFunc (oneDeriv, onePar, x);
+            for (int n = 1; n < nPar; n++) {
+                dpars[nOff + n] = dPAR[n];
+            }
+        }
+        model += value;
+    }
+    if (deriv != NULL) {
+        dpars[0] = dPAR[0]*2.0;
+    }
+    return (model);
+}
+
+/*
+i:         0           1               2 
+n:         1  2  3  4  5  6  1  2  3  4  5  6  1  2  3  4  5  6
+i*6 + n: 0 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
+*/
+
+bool pmSourceFitSet (pmSource *source,
+                     psArray *modelSet,
+                     pmSourceFitMode mode)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->mask, false);
+    PS_ASSERT_PTR_NON_NULL(source->weight, false);
+
+    psBool fitStatus = true;
+    psBool onPic     = true;
+    psBool rc        = true;
+
+    // base values on first model
+    pmModel *model = modelSet->data[0];
+
+    // set the static variables
+    pmModelFitSetInit (model->type);
+
+    int nSrc = modelSet->n;
+    int nPar = model->params->n - 1;  // number of object parameters (excluding sky)
+
+    // define parameter vectors for source set
+    psVector *params = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    psVector *dparams = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+
+    pmModelLimits modelLimits = pmModelLimits_GetFunction (model->type);
+
+    // define limits for a single-source model
+    psVector *oneDelta;
+    psVector *oneParMin;
+    psVector *oneParMax;
+    modelLimits (&oneDelta, &oneParMin, &oneParMax);
+
+    psMinConstrain *constrain = psMinConstrainAlloc();
+    constrain->paramMin = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    constrain->paramMax = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    constrain->paramMask = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_U8);
+    constrain->paramDelta = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+
+    // set the parameter guesses and constraints for the multiple models
+    // first the values for the single sky parameter
+    params->data.F32[0] = model->params->data.F32[0];
+    constrain->paramMin->data.F32[0]   = oneParMin->data.F32[0];
+    constrain->paramMax->data.F32[0]   = oneParMax->data.F32[0];
+    constrain->paramDelta->data.F32[0] = oneDelta->data.F32[0];
+
+    // next, the values for the source parameters
+    for (int i = 0; i < nSrc; i++) {
+        model = modelSet->data[i];
+        for (int n = 1; n < nPar + 1; n++) {
+            params->data.F32[i*nPar + n] = model->params->data.F32[n];
+            constrain->paramDelta->data.F32[i*nPar + n] = oneDelta->data.F32[n];
+            constrain->paramMin->data.F32[i*nPar + n]   = oneParMin->data.F32[n];
+            constrain->paramMax->data.F32[i*nPar + n]   = oneParMax->data.F32[n];
+        }
+    }
+    psFree (oneDelta);
+    psFree (oneParMin);
+    psFree (oneParMax);
+
+    if (psTraceGetLevel(__func__) >= 5) {
+        for (int i = 0; i < params->n; i++) {
+            fprintf (stderr, "%d %f %d\n", i, params->data.F32[i], constrain->paramMask->data.U8[i]);
+        }
+    }
+
+    // set the parameter masks based on the fitting mode
+    int nParams = 0;
+    switch (mode) {
+    case PM_SOURCE_FIT_NORM:
+        // NORM-only model fits only source normalization (Io)
+        nParams = nSrc;
+        psVectorInit (constrain->paramMask, 1);
+        for (int i = 0; i < nSrc; i++) {
+            constrain->paramMask->data.U8[1 + i*nPar] = 0;
+        }
+        break;
+    case PM_SOURCE_FIT_PSF:
+        // PSF model only fits x,y,Io
+        nParams = 3*nSrc;
+        psVectorInit (constrain->paramMask, 1);
+        for (int i = 0; i < nSrc; i++) {
+            constrain->paramMask->data.U8[1 + i*nPar] = 0;
+            constrain->paramMask->data.U8[2 + i*nPar] = 0;
+            constrain->paramMask->data.U8[3 + i*nPar] = 0;
+        }
+        break;
+    case PM_SOURCE_FIT_EXT:
+        // EXT model fits all params (except sky)
+        nParams = nPar*nSrc;
+        psVectorInit (constrain->paramMask, 0);
+        constrain->paramMask->data.U8[0] = 1;
+        break;
+    case PM_SOURCE_FIT_PSF_AND_SKY:
+        nParams = 1 + 3*nSrc;
+        psAbort ("pmSourceFitModel", "PSF + SKY not currently available");
+        break;
+    case PM_SOURCE_FIT_EXT_AND_SKY:
+        nParams = 1 + nPar*nSrc;
+        psAbort ("pmSourceFitModel", "EXT + SKY not currently available");
+        break;
+    default:
+        psAbort ("pmSourceFitModel", "invalid fitting mode");
+    }
+
+    // maximum number of valid pixels
+    psS32 nPix = source->pixels->numRows * source->pixels->numCols;
+
+    // local sky variance
+    psF32 dSky = source->moments->dSky;
+
+    // construct the coordinate and value entries
+    psArray *x = psArrayAlloc(nPix);
+    psVector *y = psVectorAlloc(nPix, PS_TYPE_F32);
+    psVector *yErr = psVectorAlloc(nPix, PS_TYPE_F32);
+
+    nPix = 0;
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->mask->data.U8[i][j]) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+            x->data[nPix] = (psPtr *) coord;
+            y->data.F32[nPix] = source->pixels->data.F32[i][j];
+            // psMinimizeLMChi2 takes wt = 1/dY^2
+            if (PM_PSF_POISSON_WEIGHTS) {
+                yErr->data.F32[nPix] = 1.0 / source->weight->data.F32[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / dSky;
+            }
+            nPix++;
+        }
+    }
+    if (nPix <  nParams + 1) {
+        psTrace (__func__, 4, "insufficient valid pixels\n");
+        psTrace(__func__, 3, "---- %s() end : fail pixels ----\n", __func__);
+        model->status = PM_MODEL_BADARGS;
+        psFree (x);
+        psFree (y);
+        psFree (yErr);
+        psFree (params);
+        psFree (dparams);
+        psFree(constrain->paramMask);
+        psFree(constrain->paramMin);
+        psFree(constrain->paramMax);
+        psFree(constrain->paramDelta);
+        psFree(constrain);
+        return(false);
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+    psMinimization *myMin = psMinimizationAlloc(PM_SOURCE_FIT_MODEL_NUM_ITERATIONS,
+                            PM_SOURCE_FIT_MODEL_TOLERANCE);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F64);
+
+    psTrace (__func__, 5, "fitting function\n");
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constrain, x, y, yErr, pmModelFitSet);
+
+    // parameter errors from the covariance matrix
+    for (int i = 0; i < dparams->n; i++) {
+        if ((constrain->paramMask != NULL) && constrain->paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F64[i][i]);
+    }
+
+    // get the Gauss-Newton distance for fixed model parameters
+    if (constrain->paramMask != NULL) {
+        psVector *delta = psVectorAlloc (params->n, PS_TYPE_F64);
+        psMinimizeGaussNewtonDelta(delta, params, NULL, x, y, yErr, pmModelFitSet);
+        for (int i = 0; i < dparams->n; i++) {
+            if (!constrain->paramMask->data.U8[i])
+                continue;
+            dparams->data.F32[i] = delta->data.F64[i];
+        }
+        psFree (delta);
+    }
+
+    // assign back the parameters to the models
+    for (int i = 0; i < nSrc; i++) {
+        model = modelSet->data[i];
+        model->params->data.F32[0] = params->data.F32[0];
+        for (int n = 1; n < nPar + 1; n++) {
+            model->params->data.F32[n] = params->data.F32[i*nPar + n];
+            model->dparams->data.F32[n] = dparams->data.F32[i*nPar + n];
+        }
+        // save the resulting chisq, nDOF, nIter
+        // these are not unique for any one source
+        model->chisq = myMin->value;
+        model->nIter = myMin->iter;
+        model->nDOF  = y->n - nParams;
+
+        // set the model success or failure status
+        model->status = fitStatus ? PM_MODEL_SUCCESS : PM_MODEL_NONCONVERGE;
+
+        // models can go insane: reject these
+        onPic &= (model->params->data.F32[2] >= source->pixels->col0);
+        onPic &= (model->params->data.F32[2] <  source->pixels->col0 + source->pixels->numCols);
+        onPic &= (model->params->data.F32[3] >= source->pixels->row0);
+        onPic &= (model->params->data.F32[3] <  source->pixels->row0 + source->pixels->numRows);
+        if (!onPic) {
+            model->status = PM_MODEL_OFFIMAGE;
+        }
+    }
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+
+    psFree(x);
+    psFree(y);
+    psFree(yErr);
+    psFree(myMin);
+    psFree(covar);
+    psFree(constrain->paramMask);
+    psFree(constrain->paramMin);
+    psFree(constrain->paramMax);
+    psFree(constrain->paramDelta);
+    psFree(constrain);
+    psFree(params);
+    psFree(dparams);
+
+    // free static memory used by pmModelFitSet
+    pmModelFitSetClear ();
+
+    rc = (onPic && fitStatus);
+    psTrace (__func__, 5, "onPic: %d, fitStatus: %d, nIter: %d, chisq: %f, nDof: %d\n", onPic, fitStatus, model->nIter, model->chisq, model->nDOF);
+    psTrace(__func__, 3, "---- %s end : status %d ----\n", __func__, rc);
+    return(rc);
+}
+
Index: trunk/psModules/src/objects/pmSourceFitModel.h
===================================================================
--- trunk/psModules/src/objects/pmSourceFitModel.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceFitModel.h	(revision 6872)
@@ -0,0 +1,69 @@
+/** @file  pmSourceFitModel.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_FIT_MODEL_H
+# define PM_SOURCE_FIT_MODEL_H
+
+typedef enum {
+    PM_SOURCE_FIT_NORM,
+    PM_SOURCE_FIT_PSF,
+    PM_SOURCE_FIT_EXT,
+    PM_SOURCE_FIT_PSF_AND_SKY,
+    PM_SOURCE_FIT_EXT_AND_SKY
+} pmSourceFitMode;
+
+bool pmSourceFitModelInit(
+    float nIter,   ///< max number of allowed iterations
+    float tol,      ///< convergence criterion
+    bool poissonErrors   // use poisson errors for fits?
+);
+
+/** pmSourceFitModel()
+ *
+ * Fit the requested model to the specified source. The starting guess for the model is given
+ * by the input source.model parameter values. The pixels of interest are specified by the
+ * source.pixels and source.mask entries. This function calls psMinimizeLMChi2() on the image
+ * data. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitModel(
+    pmSource *source,   ///< The input pmSource
+    pmModel *model,   ///< model to be fitted
+    pmSourceFitMode mode  ///< define parameters to be fitted
+);
+
+
+// initialize data for a group of object models
+bool pmModelFitSetInit (pmModelType type);
+
+// clear data for a group of object models
+void pmModelFitSetClear (void);
+
+// function used to fit a group of object models
+psF32 pmModelFitSet(psVector *deriv,
+                    const psVector *params,
+                    const psVector *x);
+
+/** pmSourceFitSet()
+ *
+ * Fit the requested model to the specified source. The starting guess for the model is given
+ * by the input source.model parameter values. The pixels of interest are specified by the
+ * source.pixels and source.mask entries. This function calls psMinimizeLMChi2() on the image
+ * data. The function returns TRUE on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitSet(
+    pmSource *source,   ///< The input pmSource
+    psArray *modelSet,   ///< model to be fitted
+    pmSourceFitMode mode  ///< define parameters to be fitted
+);
+
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: trunk/psModules/src/objects/pmSourceFitSet.c
===================================================================
--- trunk/psModules/src/objects/pmSourceFitSet.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceFitSet.c	(revision 6872)
@@ -0,0 +1,334 @@
+/** @file  pmSourceFitSet.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceFitSet.h"
+
+// sky, p1.1, p1.2, p1.3,... p1.n, p2.1, p2.2,
+// nPar = nSrc*(nOnePar - 1) + 1
+
+static pmModelFunc mFunc;
+static int nPar;
+static psVector *onePar;
+static psVector *oneDeriv;
+
+// save as static values so they may be set externally
+static psF32 PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = 15;
+static psF32 PM_SOURCE_FIT_MODEL_TOLERANCE = 0.1;
+static bool PM_PSF_POISSON_WEIGHTS = true;
+
+bool pmSourceFitSetInit (float nIter, float tol, bool poissonErrors)
+{
+
+    PM_SOURCE_FIT_MODEL_NUM_ITERATIONS = nIter;
+    PM_SOURCE_FIT_MODEL_TOLERANCE = tol;
+
+    PM_PSF_POISSON_WEIGHTS = poissonErrors;
+
+    return true;
+}
+
+bool pmModelFitSetInit (pmModelType type)
+{
+
+    mFunc = pmModelFunc_GetFunction (type);
+    nPar  = pmModelParameterCount (type);
+
+    onePar = psVectorAlloc (nPar, PS_DATA_F32);
+    oneDeriv = psVectorAlloc (nPar, PS_DATA_F32);
+
+    return true;
+}
+
+void pmModelFitSetClear (void)
+{
+
+    psFree (onePar);
+    psFree (oneDeriv);
+    return;
+}
+
+psF32 pmModelFitSet(psVector *deriv,
+                    const psVector *params,
+                    const psVector *x)
+{
+
+    psF32 value;
+    psF32 model;
+
+    psF32 *PAR = onePar->data.F32;
+    psF32 *dPAR = oneDeriv->data.F32;
+
+    psF32 *pars = params->data.F32;
+    psF32 *dpars = (deriv == NULL) ? NULL : deriv->data.F32;
+
+    int nSrc = (params->n - 1) / (nPar - 1);
+
+    PAR[0] = model = pars[0];
+    for (int i = 0; i < nSrc; i++) {
+        int nOff = i*nPar - i;
+        for (int n = 1; n < nPar; n++) {
+            PAR[n] = pars[nOff + n];
+        }
+        if (deriv == NULL) {
+            value = mFunc (NULL, onePar, x);
+        } else {
+            value = mFunc (oneDeriv, onePar, x);
+            for (int n = 1; n < nPar; n++) {
+                dpars[nOff + n] = dPAR[n];
+            }
+        }
+        model += value;
+    }
+    if (deriv != NULL) {
+        dpars[0] = dPAR[0]*2.0;
+    }
+    return (model);
+}
+
+/*
+i:         0           1               2 
+n:         1  2  3  4  5  6  1  2  3  4  5  6  1  2  3  4  5  6
+i*6 + n: 0 1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18
+*/
+
+/*
+# define PM_SOURCE_FIT_MODEL_NUM_ITERATIONS 15
+# define PM_SOURCE_FIT_MODEL_TOLERANCE 0.1
+*/
+
+bool pmSourceFitSet (pmSource *source,
+                     psArray *modelSet,
+                     const bool PSF)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_PTR_NON_NULL(source->moments, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_PTR_NON_NULL(source->pixels, false);
+    PS_ASSERT_PTR_NON_NULL(source->mask, false);
+    PS_ASSERT_PTR_NON_NULL(source->weight, false);
+
+    psBool fitStatus = true;
+    psBool onPic     = true;
+    psBool rc        = true;
+
+    // base values on first model
+    pmModel *model = modelSet->data[0];
+
+    // set the static variables
+    pmModelFitSetInit (model->type);
+
+    int nSrc = modelSet->n;
+    int nPar = model->params->n - 1;  // number of object parameters (excluding sky)
+
+    // define parameter vectors for source set
+    psVector *params = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    psVector *dparams = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+
+    pmModelLimits modelLimits = pmModelLimits_GetFunction (model->type);
+
+    // define limits for single-source model
+    psVector *oneDelta;
+    psVector *oneParMin;
+    psVector *oneParMax;
+    modelLimits (&oneDelta, &oneParMin, &oneParMax);
+
+    psMinConstrain *constrain = psMinConstrainAlloc();
+    constrain->paramDelta = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    constrain->paramMin = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    constrain->paramMax = psVectorAlloc (nSrc*nPar + 1, PS_TYPE_F32);
+    constrain->paramMask = PSF ? psVectorAlloc (nSrc*nPar + 1, PS_TYPE_U8) : NULL;
+
+    // all but the sky are allowed to vary independently (subject to PSF)
+    // set the parameters from the multiple models
+    // also, set limits based on single-source limits
+    params->data.F32[0] = model->params->data.F32[0];
+    constrain->paramDelta->data.F32[0] = oneDelta->data.F32[0];
+    constrain->paramMin->data.F32[0]   = oneParMin->data.F32[0];
+    constrain->paramMax->data.F32[0]   = oneParMax->data.F32[0];
+
+    // XXX all models skip the sky
+    constrain->paramMask->data.U8[0]   = 1;
+    for (int i = 0; i < nSrc; i++) {
+        model = modelSet->data[i];
+        for (int n = 1; n < nPar + 1; n++) {
+            params->data.F32[i*nPar + n] = model->params->data.F32[n];
+            dparams->data.F32[i*nPar + n] = model->dparams->data.F32[n];
+            constrain->paramDelta->data.F32[i*nPar + n] = oneDelta->data.F32[n];
+            constrain->paramMin->data.F32[i*nPar + n]   = oneParMin->data.F32[n];
+            constrain->paramMax->data.F32[i*nPar + n]   = oneParMax->data.F32[n];
+            if (PSF) {
+                constrain->paramMask->data.U8[i*nPar + n] = (n < 4) ? 0 : 1;
+            }
+        }
+    }
+    psFree (oneDelta);
+    psFree (oneParMin);
+    psFree (oneParMax);
+
+    if (psTraceGetLevel(__func__) >= 5) {
+        for (int i = 0; i < params->n; i++) {
+            fprintf (stderr, "%d %f %d\n", i, params->data.F32[i], constrain->paramMask->data.U8[i]);
+        }
+    }
+
+    // PSF model only fits first 4 parameters, EXT model fits all
+    int nParams = PSF ? nSrc*3 + 1 : nSrc*nPar + 1;
+
+    // maximum number of valid pixels
+    psS32 nPix = source->pixels->numRows * source->pixels->numCols;
+
+    // local sky variance
+    psF32 dSky = source->moments->dSky;
+
+    // construct the coordinate and value entries
+    psArray *x = psArrayAlloc(nPix);
+    psVector *y = psVectorAlloc(nPix, PS_TYPE_F32);
+    psVector *yErr = psVectorAlloc(nPix, PS_TYPE_F32);
+
+    nPix = 0;
+    for (psS32 i = 0; i < source->pixels->numRows; i++) {
+        for (psS32 j = 0; j < source->pixels->numCols; j++) {
+            // skip masked points
+            if (source->mask->data.U8[i][j]) {
+                continue;
+            }
+            // skip zero-weight points
+            if (source->weight->data.F32[i][j] == 0) {
+                continue;
+            }
+            psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+            // Convert i/j to image space:
+            coord->data.F32[0] = (psF32) (j + source->pixels->col0);
+            coord->data.F32[1] = (psF32) (i + source->pixels->row0);
+            x->data[nPix] = (psPtr *) coord;
+            y->data.F32[nPix] = source->pixels->data.F32[i][j];
+            // psMinimizeLMChi2 takes wt = 1/dY^2
+            if (PM_PSF_POISSON_WEIGHTS) {
+                yErr->data.F32[nPix] = 1.0 / source->weight->data.F32[i][j];
+            } else {
+                yErr->data.F32[nPix] = 1.0 / dSky;
+            }
+            nPix++;
+        }
+    }
+    if (nPix <  nParams + 1) {
+        psTrace (__func__, 4, "insufficient valid pixels\n");
+        psTrace(__func__, 3, "---- %s() end : fail pixels ----\n", __func__);
+        model->status = PM_MODEL_BADARGS;
+        psFree (x);
+        psFree (y);
+        psFree (yErr);
+        psFree (params);
+        psFree (dparams);
+        psFree(constrain->paramMask);
+        psFree(constrain->paramMin);
+        psFree(constrain->paramMax);
+        psFree(constrain->paramDelta);
+        psFree(constrain);
+        return(false);
+    }
+    x->n = nPix;
+    y->n = nPix;
+    yErr->n = nPix;
+
+    psMinimization *myMin = psMinimizationAlloc(PM_SOURCE_FIT_MODEL_NUM_ITERATIONS,
+                            PM_SOURCE_FIT_MODEL_TOLERANCE);
+
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F64);
+
+    psTrace (__func__, 5, "fitting function\n");
+    fitStatus = psMinimizeLMChi2(myMin, covar, params, constrain, x, y, yErr, pmModelFitSet);
+
+    // parameter errors from the covariance matrix
+    for (int i = 0; i < dparams->n; i++) {
+        if ((constrain->paramMask != NULL) && constrain->paramMask->data.U8[i])
+            continue;
+        dparams->data.F32[i] = sqrt(covar->data.F64[i][i]);
+    }
+
+    // get the Gauss-Newton distance for fixed model parameters
+    if (constrain->paramMask != NULL) {
+        psVector *delta = psVectorAlloc (params->n, PS_TYPE_F64);
+        psMinimizeGaussNewtonDelta(delta, params, NULL, x, y, yErr, pmModelFitSet);
+        for (int i = 0; i < dparams->n; i++) {
+            if (!constrain->paramMask->data.U8[i])
+                continue;
+            dparams->data.F32[i] = delta->data.F64[i];
+        }
+        psFree (delta);
+    }
+
+    // assign back the parameters to the models
+    for (int i = 0; i < nSrc; i++) {
+        model = modelSet->data[i];
+        model->params->data.F32[0] = params->data.F32[0];
+        for (int n = 1; n < nPar + 1; n++) {
+            model->params->data.F32[n] = params->data.F32[i*nPar + n];
+            model->dparams->data.F32[n] = dparams->data.F32[i*nPar + n];
+        }
+        // save the resulting chisq, nDOF, nIter
+        // these are not unique for any one source
+        model->chisq = myMin->value;
+        model->nIter = myMin->iter;
+        model->nDOF  = y->n - nParams;
+
+        // set the model success or failure status
+        model->status = fitStatus ? PM_MODEL_SUCCESS : PM_MODEL_NONCONVERGE;
+
+        // models can go insane: reject these
+        onPic &= (model->params->data.F32[2] >= source->pixels->col0);
+        onPic &= (model->params->data.F32[2] <  source->pixels->col0 + source->pixels->numCols);
+        onPic &= (model->params->data.F32[3] >= source->pixels->row0);
+        onPic &= (model->params->data.F32[3] <  source->pixels->row0 + source->pixels->numRows);
+        if (!onPic) {
+            model->status = PM_MODEL_OFFIMAGE;
+        }
+    }
+
+    source->mode |= PM_SOURCE_MODE_FITTED;
+
+    psFree(x);
+    psFree(y);
+    psFree(yErr);
+    psFree(myMin);
+    psFree(covar);
+    psFree(constrain->paramMask);
+    psFree(constrain->paramMin);
+    psFree(constrain->paramMax);
+    psFree(constrain->paramDelta);
+    psFree(constrain);
+    psFree(params);
+    psFree(dparams);
+
+    // free static memory used by pmModelFitSet
+    pmModelFitSetClear ();
+
+    rc = (onPic && fitStatus);
+    psTrace (__func__, 5, "onPic: %d, fitStatus: %d, nIter: %d, chisq: %f, nDof: %d\n", onPic, fitStatus, model->nIter, model->chisq, model->nDOF);
+    psTrace(__func__, 3, "---- %s end : status %d ----\n", __func__, rc);
+    return(rc);
+}
+
Index: trunk/psModules/src/objects/pmSourceFitSet.h
===================================================================
--- trunk/psModules/src/objects/pmSourceFitSet.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceFitSet.h	(revision 6872)
@@ -0,0 +1,40 @@
+/** @file  pmSourceFitset.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_FIT_SET_H
+# define PM_SOURCE_FIT_SET_H
+
+psF32 pmModelFitSet(psVector *deriv,
+                    const psVector *params,
+                    const psVector *x);
+
+bool pmModelFitSetInit (pmModelType type);
+
+void pmModelFitSetClear (void);
+
+bool pmSourceFitSetInit (float nIter, float tol, bool poissonErrors);
+
+/** pmSourceFitSet()
+ *
+ * Fit the requested model to the specified source. The starting guess for the
+ * model is given by the input source.model parameter values. The pixels of
+ * interest are specified by the source.pixelsand source.maskentries. This
+ * function calls psMinimizeLMChi2() on the image data. The function returns TRUE
+ * on success or FALSE on failure.
+ *
+ */
+bool pmSourceFitSet(
+    pmSource *source,   ///< The input pmSource
+    psArray *modelSet,   ///< model to be fitted
+    const bool PSF   ///< Treat model as PSF or EXT?
+);
+
+# endif /* PM_SOURCE_FIT_MODEL_H */
Index: trunk/psModules/src/objects/pmSourceIO.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO.c	(revision 6872)
@@ -0,0 +1,378 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psAdditionals.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+// translations between psphot object types and dophot object types
+int pmSourceDophotType (pmSource *source)
+{
+
+    switch (source->type) {
+
+    case PM_SOURCE_TYPE_DEFECT:
+    case PM_SOURCE_TYPE_SATURATED:
+        return (8);
+
+    case PM_SOURCE_TYPE_STAR:
+        if (source->mode & PM_SOURCE_MODE_SATSTAR)
+            return (10);
+        if (source->mode & PM_SOURCE_MODE_POOR)
+            return (7);
+        if (source->mode & PM_SOURCE_MODE_FAIL)
+            return (4);
+        return (1);
+
+    case PM_SOURCE_TYPE_EXTENDED:
+        return (2);
+
+    default:
+        return (0);
+    }
+    return (0);
+}
+
+// Given a FITS file pointer, read the table of object data
+bool pmFPAviewWriteObjects (const pmFPAview *view, pmFPAfile *file)
+{
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAWriteObjects (fpa, view, file);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipWriteObjects (chip, view, file);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellWriteObjects (cell, view, file);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutWriteObjects (readout, view, file);
+    return true;
+}
+
+// read in all chip-level Objects files for this FPA
+bool pmFPAWriteObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipWriteObjects (chip, view, file);
+    }
+    return true;
+}
+
+// read in all cell-level Objects files for this chip
+bool pmChipWriteObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellWriteObjects (cell, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmCellWriteObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutWriteObjects (readout, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutWriteObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file)
+{
+
+    bool status;
+    char *filename;
+    char *dataname;
+    char *headname;
+    pmHDU *hdu;
+    pmHDU *phu;
+    psMetadata *updates;
+    psMetadata *outhead;
+
+    psArray *sources = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.SOURCES");
+
+    switch (file->type) {
+    case PM_FPA_FILE_RAW:
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+        pmSourcesWriteRAW (sources, filename);
+        psFree (filename);
+        break;
+
+    case PM_FPA_FILE_OBJ:
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+        pmSourcesWriteOBJ (sources, filename);
+        psFree (filename);
+        break;
+
+    case PM_FPA_FILE_SX:
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+        pmSourcesWriteSX (sources, filename);
+        psFree (filename);
+        break;
+
+    case PM_FPA_FILE_CMP:
+        // a SPLIT format : only one header and object table per file
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+        filename = pmFPAfileNameFromRule (file->filerule, file, view);
+
+        // copy the header to an output header, add the output header data
+        outhead = psMetadataCopy (NULL, hdu->header);
+
+        // check/fix first line (must be SIMPLE = F)
+        psMetadataItem *item = psMetadataGet (outhead, PS_LIST_HEAD);
+        if (strcmp (item->name, "SIMPLE") && strcmp (item->name, "XTENSION")) {
+            psErrorStackPrint(stderr, "invalid header: first line is neither SIMPLE nor XTENSION\n");
+            exit(EXIT_FAILURE);
+        }
+        if (!strcmp (item->name, "XTENSION")) {
+            psMetadataRemoveIndex (outhead, PS_LIST_HEAD);
+        }
+        psMetadataAddBool (outhead, PS_LIST_HEAD, "SIMPLE", PS_META_REPLACE, "CMP file, not simple", false);
+
+        // copy over the entries saved in the
+        updates = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.HEADER");
+        psMetadataCopy (outhead, updates);
+
+        pmSourcesWriteCMP (sources, filename, outhead);
+        psFree (outhead);
+        psFree (filename);
+        break;
+
+    case PM_FPA_FILE_CMF:
+        // write a PHU?
+        // write a header?
+        // write the data
+
+        // get the current header
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        // if file does not yet have a PHU, write it to disk
+        if (file->phu == NULL) {
+            // get the corresponding phu
+            phu = pmFPAviewThisPHU (view, file->fpa);
+
+            // CMF always has extensions
+            outhead = psMetadataCopy (NULL, phu->header);
+            psMetadataAdd (outhead, PS_LIST_TAIL, "EXTEND", PS_DATA_BOOL | PS_META_REPLACE, "this file has extensions", true);
+            psFitsWriteHeaderNotImage (file->fits, outhead);
+            file->phu = phu->header;
+            psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+            psFree (outhead);
+        }
+
+        // this this header block is new, write it to disk,
+        if (hdu->header != file->header) {
+            // determine name for header extension
+            headname = pmFPAfileNameFromRule (file->extxtra, file, view);
+            outhead = psMetadataCopy (NULL, hdu->header);
+            psMetadataAddStr (outhead, PS_LIST_TAIL, "EXTNAME", PS_META_REPLACE, "extension name", headname);
+            psFitsWriteHeaderNotImage (file->fits, outhead);
+            psTrace ("pmFPAfile", 5, "wrote ext head %s (type: %d)\n", file->filename, file->type);
+            file->header = hdu->header;
+            psFree (outhead);
+            psFree (headname);
+        }
+
+        // write this table to disk
+        dataname = pmFPAfileNameFromRule (file->extrule, file, view);
+        updates = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.HEADER");
+        pmSourcesWriteCMF (file->fits, sources, updates, dataname);
+        psTrace ("pmFPAfile", 5, "wrote ext data %s (type: %d)\n", file->filename, file->type);
+        psFree (dataname);
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    return true;
+}
+// a MEF CMF file has: PHU, CELL-HEAD, TABLE, CELL-HEAD, TABLE, TABLE, TABLE...
+
+// Given a FITS file pointer, read the table of object data
+bool pmFPAviewReadObjects (const pmFPAview *view, pmFPAfile *file)
+{
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        pmFPAReadObjects (fpa, view, file);
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        pmChipReadObjects (chip, view, file);
+        return true;
+    }
+
+    if (view->cell >= chip->cells->n) {
+        return false;
+    }
+    pmCell *cell = chip->cells->data[view->cell];
+
+    if (view->readout == -1) {
+        pmCellReadObjects (cell, view, file);
+        return true;
+    }
+
+    if (view->readout >= cell->readouts->n) {
+        return false;
+    }
+    pmReadout *readout = cell->readouts->data[view->readout];
+
+    pmReadoutReadObjects (readout, view, file);
+    return true;
+}
+
+// read in all chip-level Objects files for this FPA
+bool pmFPAReadObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+
+        pmChip *chip = fpa->chips->data[i];
+        pmChipReadObjects (chip, view, file);
+    }
+    return true;
+}
+
+// read in all cell-level Objects files for this chip
+bool pmChipReadObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < chip->cells->n; i++) {
+
+        pmCell *cell = chip->cells->data[i];
+        pmCellReadObjects (cell, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmCellReadObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file)
+{
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+
+        pmReadout *readout = cell->readouts->data[i];
+        pmReadoutReadObjects (readout, view, file);
+    }
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmReadoutReadObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file)
+{
+
+    bool status;
+    psArray *sources = NULL;
+    pmHDU *hdu;
+
+    switch (file->type) {
+    case PM_FPA_FILE_OBJ:
+        fprintf (stderr, "warning: OBJ is not supported as an input object format\n");
+        break;
+
+    case PM_FPA_FILE_SX:
+        fprintf (stderr, "warning: SX is not supported as an input object format\n");
+        break;
+
+    case PM_FPA_FILE_CMP:
+        // a SPLIT format : only one header and object table per file
+
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+
+        char *filename = pmFPAfileNameFromRule (file->filerule, file, view);
+        file->fits = psFitsOpen (filename, "r");
+        hdu->header = psFitsReadHeader (NULL, file->fits);
+        psFitsClose (file->fits);
+        sources  = pmSourcesReadCMP (filename, hdu->header);
+        psFree (filename);
+        break;
+
+    case PM_FPA_FILE_CMF:
+        // read in header, if not yet loaded
+        hdu = pmFPAviewThisHDU (view, file->fpa);
+        if (hdu->header == NULL) {
+            char *headname = pmFPAfileNameFromRule (file->extxtra, file, view);
+            psFitsMoveExtName (file->fits, headname);
+            hdu->header = psFitsReadHeader (NULL, file->fits);
+            psFree (headname);
+        }
+
+        char *dataname = pmFPAfileNameFromRule (file->extrule, file, view);
+        psFitsMoveExtName (file->fits, dataname);
+        sources  = pmSourcesReadCMF (file->fits, hdu->header);
+        psFree (dataname);
+        break;
+
+    default:
+        fprintf (stderr, "warning: type mismatch\n");
+        break;
+    }
+    status = psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.SOURCES", PS_DATA_ARRAY, "psphot sources", sources);
+    return true;
+}
+
+
Index: trunk/psModules/src/objects/pmSourceIO.h
===================================================================
--- trunk/psModules/src/objects/pmSourceIO.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO.h	(revision 6872)
@@ -0,0 +1,44 @@
+/** @file  pmSourceIO.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_IO_H
+# define PM_SOURCE_IO_H
+
+int pmSourceDophotType (pmSource *source);
+
+bool pmSourcesWriteRAW (psArray *sources, char *filename);
+bool pmSourcesWriteOBJ (psArray *sources, char *filename);
+bool pmSourcesWriteSX (psArray *sources, char *filename);
+bool pmSourcesWriteCMP (psArray *sources, char *filename, psMetadata *header);
+bool pmSourcesWriteCMF (psFits *fits, psArray *sources, psMetadata *header, char *extname);
+
+psArray *pmSourcesReadCMP (char *filename, psMetadata *header);
+psArray *pmSourcesReadCMF (psFits *fits, psMetadata *header);
+
+bool pmSourcesWritePSFs (psArray *sources, char *filename);
+bool pmSourcesWriteEXTs (psArray *sources, char *filename);
+bool pmSourcesWriteNULLs (psArray *sources, char *filename);
+bool pmMomentsWriteText (psArray *sources, char *filename);
+bool pmPeaksWriteText (psArray *peaks, char *filename);
+
+bool pmFPAviewReadObjects (const pmFPAview *view, pmFPAfile *file);
+bool pmFPAReadObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file);
+bool pmChipReadObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file);
+bool pmCellReadObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file);
+bool pmReadoutReadObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file);
+
+bool pmFPAviewWriteObjects (const pmFPAview *view, pmFPAfile *file);
+bool pmFPAWriteObjects (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file);
+bool pmChipWriteObjects (pmChip *chip, const pmFPAview *view, pmFPAfile *file);
+bool pmCellWriteObjects (pmCell *cell, const pmFPAview *view, pmFPAfile *file);
+bool pmReadoutWriteObjects (pmReadout *readout, const pmFPAview *view, pmFPAfile *file);
+
+# endif /* PM_SOURCE_IO_H */
Index: trunk/psModules/src/objects/pmSourceIO_CMF.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO_CMF.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO_CMF.c	(revision 6872)
@@ -0,0 +1,161 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+// elixir-style FITS table output (header + table in 1st extension)
+// this format consists of a header derived from the image header
+// followed by a zero-size matrix, followed by the table data
+bool pmSourcesWriteCMF (psFits *fits, psArray *sources, psMetadata *header, char *extname)
+{
+
+    psArray *table;
+    psMetadata *row;
+    int i, type;
+    psF32 *PAR, *dPAR;
+    float lsky;
+    bool status;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+
+    table = psArrayAlloc (sources->n);
+    table->n = 0;
+
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceSelectModel (source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        type = pmSourceDophotType (source);
+        lsky = (PAR[0] < 1.0) ? 0.0 : log10(PAR[0]);
+
+        shape.sx  = PAR[4];
+        shape.sy  = PAR[5];
+        shape.sxy = PAR[6];
+        axes = psEllipseShapeToAxes (shape);
+
+        row = psMetadataAlloc ();
+        psMetadataAdd (row, PS_LIST_TAIL, "X_PIX",   PS_DATA_F32, "", PAR[2]);
+        psMetadataAdd (row, PS_LIST_TAIL, "Y_PIX",   PS_DATA_F32, "", PAR[3]);
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_RAW", PS_DATA_F32, "", PS_MIN (99.0, source->psfMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_GAL", PS_DATA_F32, "", PS_MIN (99.0, source->extMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_AP",  PS_DATA_F32, "", PS_MIN (99.0, source->apMag + ZERO_POINT));
+        psMetadataAdd (row, PS_LIST_TAIL, "MAG_ERR", PS_DATA_F32, "", PS_MIN (999, 1000*source->errMag));
+        psMetadataAdd (row, PS_LIST_TAIL, "LOG_SKY", PS_DATA_F32, "", lsky);
+        psMetadataAdd (row, PS_LIST_TAIL, "FWHM_X",  PS_DATA_F32, "", axes.major);
+        psMetadataAdd (row, PS_LIST_TAIL, "FWHM_Y",  PS_DATA_F32, "", axes.minor);
+        psMetadataAdd (row, PS_LIST_TAIL, "THETA",   PS_DATA_F32, "", axes.theta);
+        psMetadataAdd (row, PS_LIST_TAIL, "DOPHOT",  PS_DATA_U8,  "", type);
+        psMetadataAdd (row, PS_LIST_TAIL, "WEIGHT",  PS_DATA_U8,  "", PS_MIN (255, PS_MAX(0, 255*source->pixWeight)));
+        psMetadataAdd (row, PS_LIST_TAIL, "DUMMY",   PS_DATA_STRING, "", "12");
+
+        psArrayAdd (table, 100, row);
+        psFree (row);
+    }
+
+    // create the basic table header
+    psMetadataAdd (header, PS_LIST_HEAD, "EXTNAME", PS_DATA_STRING | PS_META_REPLACE, "extension name", extname);
+
+    // XXX this is broken : does not write the existing header elements except EXTNAME
+    psFitsWriteTable (fits, header, table);
+    psTrace ("pmFPAfile", 5, "wrote ext data %s\n", extname);
+
+    psFree (table);
+    return true;
+}
+
+// read in a readout from the fits file
+psArray *pmSourcesReadCMF (psFits *fits, psMetadata *header)
+{
+
+    bool status;
+    psF32 *PAR, *dPAR;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+    float lsky;
+
+    // define PSF model type
+    int modelType = pmModelSetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelSetType (PSF_NAME);
+    }
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    psArray *table = psFitsReadTable (fits);
+    // validate a single row of the table (must match SMP)
+
+    psArray *sources = psArrayAlloc (table->n);
+
+    // convert the table to the pmSource entries
+    // XXX need to chooose PSF vs EXT, based on type?
+    for (int i = 0; i < table->n; i++) {
+        pmSource *source = pmSourceAlloc ();
+        pmModel *model = pmModelAlloc (modelType);
+        source->modelPSF  = model;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        psMetadata *row = table->data[i];
+
+        lsky           = psMetadataLookupF32 (&status, row, "LOG_SKY");
+        PAR[0]         = pow(lsky, 10.0);
+        PAR[2]         = psMetadataLookupF32 (&status, row, "X_PIX");
+        PAR[3]         = psMetadataLookupF32 (&status, row, "Y_PIX");
+        axes.major     = psMetadataLookupF32 (&status, row, "FWHM_X");
+        axes.minor     = psMetadataLookupF32 (&status, row, "FWHM_Y");
+        axes.theta     = psMetadataLookupF32 (&status, row, "THETA");
+
+        shape = psEllipseAxesToShape (axes);
+
+        PAR[4] = shape.sx;
+        PAR[5] = shape.sy;
+        PAR[6] = shape.sxy;
+
+        source->psfMag = psMetadataLookupF32 (&status, row, "MAG_RAW") - ZERO_POINT;
+        source->extMag = psMetadataLookupF32 (&status, row, "MAG_GAL") - ZERO_POINT;
+        source->errMag = psMetadataLookupF32 (&status, row, "MAG_ERR");
+        source->apMag  = psMetadataLookupF32 (&status, row, "MAG_AP")  - ZERO_POINT;
+
+        sources->data[i] = source;
+    }
+    return (sources);
+}
Index: trunk/psModules/src/objects/pmSourceIO_CMP.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO_CMP.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO_CMP.c	(revision 6872)
@@ -0,0 +1,250 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psLine.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+// elixir-style pseudo FITS table (header + ascii list)
+bool pmSourcesWriteCMP (psArray *sources, char *filename, psMetadata *header)
+{
+
+    int i, type, nSrc;
+    psMetadataItem *mdi;
+    psF32 *PAR, *dPAR;
+    float lsky = 0;
+    bool status;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    // count the number of sources which will be written
+    for (i = nSrc = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceSelectModel (source);
+        if (model == NULL)
+            continue;
+        nSrc ++;
+    }
+    psMetadataAdd (header, PS_LIST_TAIL, "NSTARS", PS_DATA_S32 | PS_META_REPLACE, "NUMBER OF STARS", nSrc);
+
+    // set NAXIS to 0 : CFITSIO requires isolated header to have NAXIS = 0
+    mdi = psMetadataLookup (header, "NAXIS");
+    mdi->data.S32 = 0;
+    mdi->type = PS_DATA_S32;
+
+    // create file, write-out header
+    psFits *fits = psFitsOpen (filename, "w");
+    psFitsWriteHeader (header, fits);
+    psFitsClose (fits);
+
+    // re-open, add data to end of file
+    FILE *f = fopen (filename, "a+");
+    if (f == NULL) {
+        psLogMsg ("WriteSourceOBJ", 3, "can't open output file for output %s\n", filename);
+        return false;
+    }
+    fseek (f, 0, SEEK_END);
+
+    psLine *line = psLineAlloc (67);  // 66 is imclean-defined line length
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceSelectModel (source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        type = pmSourceDophotType (source);
+        lsky = (PAR[0] < 1.0) ? 0.0 : log10(PAR[0]);
+
+        shape.sx  = PAR[4];
+        shape.sy  = PAR[5];
+        shape.sxy = PAR[6];
+        axes = psEllipseShapeToAxes (shape);
+
+        psLineInit (line);
+        psLineAdd (line, "%6.1f ",  PAR[2]);
+        psLineAdd (line, "%6.1f ",  PAR[3]);
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->psfMag + ZERO_POINT));
+        psLineAdd (line, "%03d ",   PS_MIN (999, (int)(1000*source->errMag)));
+        psLineAdd (line, "%2d ",    type);
+        psLineAdd (line, "%3.1f ",  lsky);
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->extMag + ZERO_POINT));
+        psLineAdd (line, "%6.3f ",  PS_MIN (99.0, source->apMag  + ZERO_POINT));
+        psLineAdd (line, "%6.2f ",  axes.major);
+        psLineAdd (line, "%6.2f ",  axes.minor);
+        psLineAdd (line, "%5.1f\n", axes.theta);
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    return true;
+}
+
+# define BYTES_STAR 66
+# define BLOCK 1000
+
+// elixir-style pseudo FITS table (header + ascii list)
+psArray *pmSourcesReadCMP (char *filename, psMetadata *header)
+{
+
+    int Ninstar;
+    psF32 *PAR, *dPAR;
+    bool status;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    // define PSF model type
+    int modelType = pmModelSetType ("PS_MODEL_GAUSS");
+
+    char *PSF_NAME = psMetadataLookupStr (&status, header, "PSF_NAME");
+    if (PSF_NAME != NULL) {
+        modelType = pmModelSetType (PSF_NAME);
+    }
+
+    // find config information for output header
+    float ZERO_POINT = psMetadataLookupF32 (&status, header, "ZERO_PT");
+    if (!status)
+        ZERO_POINT = 25.0;
+
+    // how many lines in the header?
+    int nLines = header->list->n;
+    int nBytes = nLines * 80;
+    if (nBytes % 2880) {
+        int nBlock = 1 + (int)(nBytes / 2880);
+        nBytes = nBlock * 2880;
+    }
+
+    // re-open, seek to end of header
+    FILE *f = fopen (filename, "r");
+    if (f == NULL) {
+        psLogMsg ("pmSourcesReadCMP", 3, "can't open output file for input %s\n", filename);
+        return NULL;
+    }
+    fseek (f, nBytes, SEEK_SET);
+
+    // prepare array to store data
+    int nStars = psMetadataLookupS32 (&status, header, "NSTARS");
+    psArray *sources = psArrayAlloc (nStars);
+    sources->n = 0;
+
+    // we have fixed bytes / line : use that info
+    char *buffer = psAlloc (BYTES_STAR*nStars);
+
+    int Nextra = 0;
+    while (true) {
+        /* load next data block */
+        int Nbytes = BYTES_STAR * BLOCK - Nextra;
+        int nbytes = fread (&buffer[Nextra], 1, Nbytes, f);
+        if (nbytes == 0) {
+            goto done_load;
+        }
+        nbytes += Nextra;
+
+        /* check line-by-line integrity */
+        char *c  = buffer;
+        char *c2 = NULL;
+        while (c < buffer + nbytes) {
+            for (c2 = c; *c2 == '\n'; c2++)
+                ;
+            if (c2 > c) { /* extra return chars */
+                memmove (c, c2, (int)(buffer + nbytes - c2));
+                int Nskip = c2 - c;
+                nbytes -= Nskip;
+                bzero (buffer + nbytes, Nskip);
+                psLogMsg (__func__, 4, "deleted %d extra return chars\n", Nskip);
+            }
+            c2 = strchr (c, '\n');
+            if (c2 == (char *) NULL) {
+                goto done_check;
+            }
+            c2++;
+            if ((c2 - c) != BYTES_STAR) { /* bad line, delete it */
+                memmove (c, c2, (int)(buffer + nbytes - c2));
+                int Nskip = c2 - c;
+                nbytes -= Nskip;
+                bzero (buffer + nbytes, Nskip);
+                psLogMsg (__func__, 4, "deleted line, %d extra chars\n", Nskip);
+            } else {
+                c = c2;
+            }
+        }
+done_check:
+
+        /* extract data for stars */
+        Ninstar = nbytes / BYTES_STAR;
+        Nextra = nbytes % BYTES_STAR;
+        for (int j = 0; j < Ninstar; j++) {
+            psString line = psStringNCopy (&buffer[j*BYTES_STAR], BYTES_STAR);
+            psList *list = psStringSplit (line, " ");
+            psArray *array = psListToArray (list);
+
+            pmSource *source = pmSourceAlloc ();
+            pmModel *model = pmModelAlloc (modelType);
+            source->modelPSF  = model;
+
+            PAR = model->params->data.F32;
+            dPAR = model->dparams->data.F32;
+
+            PAR[0]         = pow (atof (array->data[5]), 10.0);
+            PAR[2]         = atof (array->data[0]);
+            PAR[3]         = atof (array->data[1]);
+            source->psfMag = atof (array->data[2]);
+            source->extMag = atof (array->data[6]);
+            source->errMag = atof (array->data[3]) / 1000.0;
+            source->apMag  = atof (array->data[7]);
+            axes.major     = atof (array->data[8]);
+            axes.minor     = atof (array->data[9]);
+            axes.theta  = atof (array->data[10]);
+
+            shape = psEllipseAxesToShape (axes);
+
+            PAR[4] = shape.sx;
+            PAR[5] = shape.sy;
+            PAR[6] = shape.sxy;
+
+            // source->type   = atof (array->data[4]);
+
+            psArrayAdd (sources, 100, source);
+        }
+    }
+done_load:
+
+    // XXX free up temp memory in this block
+    // XXX if sources->n != nStars, give an error?
+
+    fclose (f);
+    return (sources);
+}
Index: trunk/psModules/src/objects/pmSourceIO_OBJ.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO_OBJ.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO_OBJ.c	(revision 6872)
@@ -0,0 +1,94 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psLine.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+// dophot-style output list with fixed line width
+bool pmSourcesWriteOBJ (psArray *sources, char *filename)
+{
+
+    int type;
+    psF32 *PAR, *dPAR;
+    float dmag, apResid;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    psTimerStart ("string");
+
+    psLine *line = psLineAlloc (104);  // 104 is dophot-defined line length
+
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for output %s\n", filename);
+        return false;
+    }
+
+    // write sources with models
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceSelectModel (source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        dmag = dPAR[1] / PAR[1];
+        type = pmSourceDophotType (source);
+        if ((source->apMag < 99.0) && (source->psfMag < 99.0)) {
+            apResid = source->apMag - source->psfMag;
+        } else {
+            apResid = 0.0;
+        }
+
+        shape.sx  = PAR[4];
+        shape.sy  = PAR[5];
+        shape.sxy = PAR[6];
+        axes = psEllipseShapeToAxes (shape);
+
+        psLineInit (line);
+        psLineAdd (line, "%3d",   type);
+        psLineAdd (line, "%8.2f", PAR[2]);
+        psLineAdd (line, "%8.2f", PAR[3]);
+        psLineAdd (line, "%8.3f", source->psfMag);
+        psLineAdd (line, "%6.3f", dmag);
+        psLineAdd (line, "%9.2f", PAR[0]);
+        psLineAdd (line, "%9.3f", axes.major);
+        psLineAdd (line, "%9.3f", axes.minor);
+        psLineAdd (line, "%7.2f", axes.theta);
+        psLineAdd (line, "%8.3f", source->extMag);
+        psLineAdd (line, "%8.3f", source->apMag);
+        psLineAdd (line, "%8.2f\n", apResid);
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    fprintf (stderr, "%f seconds for %d objects\n", psTimerMark ("string"), (int)sources->n);
+    return true;
+}
Index: trunk/psModules/src/objects/pmSourceIO_RAW.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO_RAW.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO_RAW.c	(revision 6872)
@@ -0,0 +1,268 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psLine.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourcePhotometry.h"
+#include "pmSourceIO.h"
+
+/***** Text Output Methods *****/
+bool pmSourcesWriteRAW (psArray *sources, char *filename)
+{
+
+    char *name = (char *) psAlloc (strlen(filename) + 10);
+
+    sprintf (name, "%s.psf.dat", filename);
+    pmSourcesWritePSFs (sources, name);
+
+    sprintf (name, "%s.ext.dat", filename);
+    pmSourcesWriteEXTs (sources, name);
+
+    sprintf (name, "%s.nul.dat", filename);
+    pmSourcesWriteNULLs (sources, name);
+
+    sprintf (name, "%s.mnt.dat", filename);
+    pmMomentsWriteText (sources, name);
+
+    psFree (name);
+    return true;
+}
+
+// write the PSF sources to an output file
+bool pmSourcesWritePSFs (psArray *sources, char *filename)
+{
+
+    double dPos;
+    int i, j;
+    FILE *f;
+    psF32 *PAR, *dPAR;
+    pmModel  *model;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for PSFs: %s\n", filename);
+        return false;
+    }
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        if (source->type != PM_SOURCE_TYPE_STAR)
+            continue;
+        model = source->modelPSF;
+        if (model == NULL)
+            continue;
+
+        PAR  = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // dPos is positional error, dMag is mag error
+        dPos = hypot (dPAR[2], dPAR[3]);
+
+        fprintf (f, "%7.1f %7.1f  %7.1f %8.4f  %7.4f %7.4f  ",
+                 PAR[2], PAR[3], PAR[0], source->psfMag, source->errMag, dPos);
+
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", PAR[j]);
+        }
+        fprintf (f, " : ");
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", dPAR[j]);
+        }
+        fprintf (f, ": %8.4f %2d %#5x %7.3f %7.3f  %7.1f %7.2f %4.2f %4d %2d\n",
+                 source[0].apMag, source[0].type, source[0].mode,
+                 log10(model[0].chisq/model[0].nDOF),
+                 log10(model[0].chisqNorm/model[0].nDOF),
+                 source[0].moments->SN,
+                 model[0].radiusTMP,
+                 source[0].pixWeight,
+                 model[0].nDOF,
+                 model[0].nIter);
+    }
+    fclose (f);
+    return true;
+}
+
+// dump the sources to an output file
+bool pmSourcesWriteEXTs (psArray *sources, char *filename)
+{
+
+    double dPos;
+    int i, j;
+    FILE *f;
+    psF32 *PAR, *dPAR;
+    pmModel  *model;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmModelWriteEXTs", 3, "can't open output file for moments%s\n", filename);
+        return false;
+    }
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+
+        if (source->type != PM_SOURCE_TYPE_EXTENDED)
+            continue;
+
+        model = source->modelEXT;
+        if (model == NULL)
+            continue;
+
+        PAR  = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // dPos is shape error
+        // XXX these are hardwired for SGAUSS
+        dPos = hypot ((dPAR[4] / PAR[4]), (dPAR[5] / PAR[5]));
+
+        fprintf (f, "%7.1f %7.1f  %7.1f %8.4f  %7.4f %7.4f  ",
+                 PAR[2], PAR[3], PAR[0], source->extMag, source->errMag, dPos);
+
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", PAR[j]);
+        }
+        fprintf (f, " : ");
+        for (j = 4; j < model->params->n; j++) {
+            fprintf (f, "%9.6f ", dPAR[j]);
+        }
+        fprintf (f, ": %7.4f  %2d %#5x %7.3f %7.3f  %7.1f %7.2f %4.2f %4d %2d\n",
+                 source->apMag,
+                 source[0].type, source[0].mode,
+                 log10(model[0].chisq/model[0].nDOF),
+                 log10(model[0].chisqNorm/model[0].nDOF),
+                 source[0].moments->SN,
+                 model[0].radiusTMP,
+                 source[0].pixWeight,
+                 model[0].nDOF,
+                 model[0].nIter);
+    }
+    fclose (f);
+    return true;
+}
+
+// dump the sources to an output file
+bool pmSourcesWriteNULLs (psArray *sources, char *filename)
+{
+
+    int i;
+    FILE *f;
+    pmMoments *moment = NULL;
+    pmSource *source = NULL;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("DumpObjects", 3, "can't open output file for moments%s\n", filename);
+        return false;
+    }
+
+    pmMoments *empty = pmMomentsAlloc ();
+
+    // write sources with models first
+    for (i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+
+        // skip these sources (in PSF or EXT)
+        if (source->type == PM_SOURCE_TYPE_STAR)
+            continue;
+        if (source->type == PM_SOURCE_TYPE_EXTENDED)
+            continue;
+
+        if (source->moments == NULL) {
+            moment = empty;
+        } else {
+            moment = source->moments;
+        }
+
+        fprintf (f, "%5d %5d  %7.1f  %7.1f %7.1f  %6.3f %6.3f  %8.1f %7.1f %7.1f %7.1f  %4d %2d\n",
+                 source->peak->x, source->peak->y, source->peak->counts,
+                 source->moments->x, source->moments->y,
+                 source->moments->Sx, source->moments->Sy,
+                 source->moments->Sum, source->moments->Peak,
+                 source->moments->Sky, source->moments->SN,
+                 source->moments->nPixels, source->type);
+    }
+    fclose (f);
+    psFree (empty);
+    return true;
+}
+
+// write the moments to an output file
+bool pmMomentsWriteText (psArray *sources, char *filename)
+{
+
+    int i;
+    FILE *f;
+    pmSource *source = NULL;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmMomentsWriteText", 3, "can't open output file for moments%s\n", filename);
+        return false;
+    }
+
+    for (i = 0; i < sources->n; i++) {
+        source = sources->data[i];
+        if (source->moments == NULL)
+            continue;
+        fprintf (f, "%5d %5d  %7.1f  %7.1f %7.1f  %6.3f %6.3f  %10.1f %7.1f %7.1f %7.1f  %4d %2d %#5x\n",
+                 source->peak->x, source->peak->y, source->peak->counts,
+                 source->moments->x, source->moments->y,
+                 source->moments->Sx, source->moments->Sy,
+                 source->moments->Sum, source->moments->Peak,
+                 source->moments->Sky, source->moments->SN,
+                 source->moments->nPixels, source->type, source->mode);
+    }
+    fclose (f);
+    return true;
+}
+
+// write the peaks to an output file
+bool pmPeaksWriteText (psArray *peaks, char *filename)
+{
+
+    int i;
+    FILE *f;
+
+    f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg ("pmPeaksWriteText", 3, "can't open output file for peaks%s\n", filename);
+        return false;
+    }
+
+    for (i = 0; i < peaks->n; i++) {
+        pmPeak *peak = peaks->data[i];
+        if (peak == NULL)
+            continue;
+        fprintf (f, "%5d %5d  %7.1f\n",
+                 peak->x, peak->y, peak->counts);
+    }
+    fclose (f);
+    return true;
+}
+
Index: trunk/psModules/src/objects/pmSourceIO_SX.c
===================================================================
--- trunk/psModules/src/objects/pmSourceIO_SX.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceIO_SX.c	(revision 6872)
@@ -0,0 +1,86 @@
+/** @file  pmSourceIO.c
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "psLine.h"
+#include "psEllipse.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourceIO.h"
+
+// elixir-mode / sextractor-style output list with fixed line width
+bool pmSourcesWriteSX (psArray *sources, char *filename)
+{
+
+    psF32 *PAR, *dPAR;
+    psEllipseShape shape;
+    psEllipseAxes axes;
+
+    psLine *line = psLineAlloc (110);  // 110 is sextractor line length
+
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+        psLogMsg (__func__, 3, "can't open output file for output %s\n", filename);
+        return false;
+    }
+
+    // write sources with models
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = (pmSource *) sources->data[i];
+        pmModel *model = pmSourceSelectModel (source);
+        if (model == NULL)
+            continue;
+
+        PAR = model->params->data.F32;
+        dPAR = model->dparams->data.F32;
+
+        // pmSourceSextractType (source, &type, &flags);
+
+        shape.sx  = PAR[4];
+        shape.sy  = PAR[5];
+        shape.sxy = PAR[6];
+        axes = psEllipseShapeToAxes (shape);
+
+        psLineInit (line);
+        psLineAdd (line, "%5.2f",  0.0); // should be type
+        psLineAdd (line, "%11.3f", PAR[2]);
+        psLineAdd (line, "%11.3f", PAR[3]);
+        psLineAdd (line, "%9.4f",  source->psfMag);
+        psLineAdd (line, "%9.4f",  source->errMag);
+        psLineAdd (line, "%13.4f", PAR[0]);
+        psLineAdd (line, "%9.2f",  axes.major);
+        psLineAdd (line, "%9.2f",  axes.minor);
+        psLineAdd (line, "%6.1f",  axes.theta);
+        psLineAdd (line, "%9.4f",  source->extMag);
+        psLineAdd (line, "%9.4f",  source->apMag);
+        psLineAdd (line, "%4d\n",  0); // should be flags
+        fwrite (line->line, 1, line->Nline, f);
+    }
+    fclose (f);
+    psFree (line);
+    return true;
+}
+
+// XXX need to fix the FWHM / shape stuff,
+// XXX make sure we are using the correct mags, etc
Index: trunk/psModules/src/objects/pmSourcePhotometry.c
===================================================================
--- trunk/psModules/src/objects/pmSourcePhotometry.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourcePhotometry.c	(revision 6872)
@@ -0,0 +1,387 @@
+/** @file  pmSourcePhotometry.c
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmGrowthCurve.h"
+#include "pmModel.h"
+#include "pmPSF.h"
+#include "pmSource.h"
+#include "pmModelGroup.h"
+#include "pmSourcePhotometry.h"
+
+# define DO_SKY 0
+
+static float AP_MIN_SN = 0.0;
+
+bool pmSourceMagnitudesInit (psMetadata *config)
+{
+
+    bool status;
+
+    float limit = psMetadataLookupF32 (&status, config, "AP_MIN_SN");
+    if (status) {
+        AP_MIN_SN = limit;
+    }
+    return true;
+}
+
+/**
+    this function is used to calculate the three defined source magnitudes:
+    - apMag  : only if S/N > AP_MIN_SN
+             : is optionally corrected for curve-of-growth if:
+        - the source is a STAR (PSF)
+        - the option is selected (how??)
+    - psfMag : all sources with non-NULL modelPSF
+             : is optionally corrected for aperture residual if:
+        - the source is a STAR (PSF)
+        - the option is selected (how??)
+       
+    - extMag : all sources with non-NULL modelEXT 
+**/
+
+// XXX EAM : masked region should be (optionally) elliptical
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode)
+{
+
+    int status = false;
+    bool isPSF;
+    float x, y;
+    float rflux;
+    float SN;
+    pmModel *model;
+
+    source->psfMag = 99.0;
+    source->extMag = 99.0;
+    source->errMag = 99.0;
+    source->apMag  = 99.0;
+
+    switch (source->type) {
+    case PM_SOURCE_TYPE_STAR:
+        model = source->modelPSF;
+        if (model == NULL)
+            return false;
+        isPSF = true;
+        break;
+
+    case PM_SOURCE_TYPE_EXTENDED:
+        model = source->modelEXT;
+        if (model == NULL)
+            return false;
+        isPSF = false;
+        break;
+
+    default:
+        return false;
+    }
+
+    if (model->dparams->data.F32[1] > 0) {
+        SN = model->params->data.F32[1] / model->dparams->data.F32[1];
+        source->errMag = 1.0 / SN;
+    } else {
+        SN = 0.0;
+        source->errMag = 0.0;
+    }
+    x = model->params->data.F32[2];
+    y = model->params->data.F32[3];
+
+    // measure object model photometry
+    status = pmSourcePhotometryModel (&source->psfMag, source->modelPSF);
+    status = pmSourcePhotometryModel (&source->extMag, source->modelEXT);
+    // for PSFs, correct both apMag and psfMag to same system, consistent with infinite flux star in aperture RADIUS
+    if ((mode & PM_SOURCE_PHOT_APCORR) && isPSF && psf) {
+        // convert to the equivalent 2D model?
+        source->psfMag += psPolynomial4DEval (psf->ApTrend, x, y, 0.0, 0.0);
+    }
+
+    if (SN < AP_MIN_SN)
+        return false;
+
+    // replace source flux
+    // XXX test to see if source has been subtracted?
+    pmModelAdd (source->pixels, source->mask, model, false, false);
+
+    // set aperture mask circle to model radius
+    psImageKeepCircle (source->mask, x, y, model->radiusTMP, "OR", PM_SOURCE_MASK_MARKED);
+
+    // measure the weight of included pixels
+    if (mode & PM_SOURCE_PHOT_WEIGHT) {
+        pmSourcePixelWeight (&source->pixWeight, model, source->pixels, source->mask);
+    }
+
+    // measure object aperture photometry
+    status = pmSourcePhotometryAper  (&source->apMag, model, source->pixels, source->mask);
+
+    // for PSFs, correct both apMag and psfMag to same system, consistent with infinite flux star in aperture RADIUS
+    if (status && isPSF && psf) {
+        if (psf->growth && (mode & PM_SOURCE_PHOT_GROWTH)) {
+            source->apMag += pmGrowthCurveCorrect (psf->growth, model->radiusTMP);
+        }
+        if (mode & PM_SOURCE_PHOT_APCORR) {
+            rflux   = pow (10.0, 0.4*source->psfMag);
+            source->apMag  -= PS_SQR(model->radiusTMP)*rflux * psf->skyBias + psf->skySat / rflux;
+        }
+    }
+
+    // unmask aperture
+    psImageKeepCircle (source->mask, x, y, model->radiusTMP, "AND", ~PM_SOURCE_MASK_MARKED);
+
+    // subtract object, leave local sky
+    pmModelSub (source->pixels, source->mask, model, false, false);
+
+    return status;
+}
+
+/*
+aprMag' - fitMag = flux*skySat + r^2*rflux*skyBias + ApTrend(x,y)
+(aprMag - flux*skySat - r^2*rflux*skyBias) - fitMAg = ApTrend(x,y)
+(aprMag - flux*skySat - r^2*rflux*skyBias) = fitMAg + ApTrend(x,y)
+ 
+*/
+
+// return source model magnitude
+bool pmSourcePhotometryModel (float *fitMag, pmModel *model)
+{
+
+    float fitSum = 0;
+    *fitMag = 99.0;
+
+    if (model == NULL) {
+        return false;
+    }
+
+    // measure fitMag
+    pmModelFlux modelFluxFunc = pmModelFlux_GetFunction (model->type);
+    fitSum = modelFluxFunc (model->params);
+    if (fitSum <= 0)
+        return false;
+    if (!isfinite(fitSum))
+        return false;
+    *fitMag = -2.5*log10(fitSum);
+
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourcePhotometryAper (float *apMag, pmModel *model, psImage *image, psImage *mask)
+{
+    float apSum = 0;
+    float sky = 0;
+    *apMag = 99.0;
+
+    if (model == NULL) {
+        return false;
+    }
+
+    if (DO_SKY) {
+        sky = model->params->data.F32[0];
+    } else {
+        sky = 0;
+    }
+
+    // measure apMag
+    for (int ix = 0; ix < image->numCols; ix++) {
+        for (int iy = 0; iy < image->numRows; iy++) {
+            if (mask->data.U8[iy][ix])
+                continue;
+            apSum += image->data.F32[iy][ix] - sky;
+        }
+    }
+    if (apSum <= 0)
+        return false;
+
+    *apMag = -2.5*log10(apSum);
+    return (true);
+}
+
+// return source aperture magnitude
+bool pmSourcePixelWeight (float *pixWeight, pmModel *model, psImage *image, psImage *mask)
+{
+    float modelSum = 0;
+    float validSum = 0;
+    float sky = 0;
+    float value;
+
+    int Xo, Yo, dP;
+    int dX, DX, NX;
+    int dY, DY, NY;
+
+    *pixWeight = 0.0;
+
+    if (model == NULL)
+        return false;
+
+    // we only care about the value of the object model, not the local sky
+    if (DO_SKY) {
+        sky = model->params->data.F32[0];
+    } else {
+        sky = 0;
+    }
+
+    // the model function returns the source flux at a position
+    pmModelFunc modelFunc = pmModelFunc_GetFunction (model->type);
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+    psVector *params = model->params;
+
+    Xo = params->data.F32[2];
+    Yo = params->data.F32[3];
+
+    dX = Xo - image->col0;
+    dP = image->numCols - dX;
+    DX = PS_MAX(dX, dP);
+    NX = image->numCols;
+
+    dY = Yo - image->row0;
+    dP = image->numRows - dY;
+    DY = PS_MAX(dY, dP);
+    NY = image->numRows;
+
+    // measure modelSum and validSum
+    // XXX this does not work for sources near the edge: we need to measure for
+    // a full square region
+    for (int ix = -DX; ix < DX + 1; ix++) {
+        int mx = ix + dX;
+        for (int iy = -DY; iy < DY + 1; iy++) {
+            int my = iy + dY;
+
+            coord->data.F32[0] = (psF32) (ix + Xo);
+            coord->data.F32[1] = (psF32) (iy + Yo);
+
+            // for the full model, add all points
+            value = modelFunc (NULL, params, coord) - sky;
+            modelSum += value;
+
+            // include count only the unmasked pixels within the image area
+            if (mx < 0)
+                continue;
+            if (my < 0)
+                continue;
+            if (mx >= NX)
+                continue;
+            if (my >= NY)
+                continue;
+            if (mask->data.U8[my][mx])
+                continue;
+
+            validSum += value;
+        }
+    }
+    if (validSum <= 0)
+        return false;
+
+    *pixWeight = validSum / modelSum;
+    return (true);
+}
+
+float pmSourceCrossProduct (pmSource *Mi, pmSource *Mj)
+{
+
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    float flux, wt;
+
+    psImage *Pi = Mi->pixels;
+    psImage *Pj = Mj->pixels;
+
+    psImage *Wi = Mi->weight;
+
+    psImage *Ti = Mi->mask;
+    psImage *Tj = Mj->mask;
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that this is addressing the same image pixels,
+    // though only if both are source not model images
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+            wt = Wi->data.F32[yi][xi];
+            if (wt > 0) {
+                flux += (Pi->data.F32[yi][xi] * Pj->data.F32[yj][xj]) / wt;
+            }
+        }
+    }
+    return (flux);
+}
+
+float pmSourceCrossWeight (pmSource *Mi, pmSource *Mj)
+{
+
+    int Xs, Xe, Ys, Ye;
+    int xi, xj, yi, yj;
+    int xIs, xJs, yIs, yJs;
+    int xIe, yIe;
+    float flux, wt;
+
+    psImage *Pi = Mi->pixels;
+    psImage *Pj = Mj->pixels;
+
+    psImage *Wi = Mi->weight;
+
+    psImage *Ti = Mi->mask;
+    psImage *Tj = Mj->mask;
+
+    Xs = PS_MAX (Pi->col0, Pj->col0);
+    Xe = PS_MIN (Pi->col0 + Pi->numCols, Pj->col0 + Pj->numCols);
+
+    Ys = PS_MAX (Pi->row0, Pj->row0);
+    Ye = PS_MIN (Pi->row0 + Pi->numRows, Pj->row0 + Pj->numRows);
+
+    xIs = Xs - Pi->col0;
+    xJs = Xs - Pj->col0;
+    yIs = Ys - Pi->row0;
+    yJs = Ys - Pj->row0;
+
+    xIe = Xe - Pi->col0;
+    yIe = Ye - Pi->row0;
+
+    // note that this is addressing the same image pixels,
+    // though only if both are source not model images
+    flux = 0;
+    for (yi = yIs, yj = yJs; yi < yIe; yi++, yj++) {
+        for (xi = xIs, xj = xJs; xi < xIe; xi++, xj++) {
+            if (Ti->data.U8[yi][xi])
+                continue;
+            if (Tj->data.U8[yj][xj])
+                continue;
+            wt = Wi->data.F32[yi][xi];
+            if (wt > 0) {
+                flux += 1.0 / wt;
+            }
+        }
+    }
+    return (flux);
+}
+
Index: trunk/psModules/src/objects/pmSourcePhotometry.h
===================================================================
--- trunk/psModules/src/objects/pmSourcePhotometry.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourcePhotometry.h	(revision 6872)
@@ -0,0 +1,53 @@
+/** @file  pmSourcePhotometry.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_PHOTOMETRY_H
+# define PM_SOURCE_PHOTOMETRY_H
+
+/**
+ *
+ * The function returns both the magnitude of the fit, defined as -2.5log(flux),
+ * where the flux is integrated under the model, theoretically from a radius of 0
+ * to infinity. In practice, we integrate the model beyond 50sigma.  The aperture magnitude is
+ * defined as -2.5log(flux) , where the flux is summed for all pixels which are
+ * not excluded by the aperture mask. The model flux is calculated by calling the
+ * model-specific function provided by pmModelFlux_GetFunction.
+ *
+ * XXX: must code this.
+ *
+ */
+
+typedef enum {
+    PM_SOURCE_PHOT_NONE   = 0x0000,
+    PM_SOURCE_PHOT_GROWTH = 0x0001,
+    PM_SOURCE_PHOT_APCORR = 0x0002,
+    PM_SOURCE_PHOT_WEIGHT = 0x0004,
+} pmSourcePhotometryMode;
+
+bool pmSourcePhotometryModel(
+    float *fitMag,                      ///< integrated fit magnitude
+    pmModel *model                      ///< model used for photometry
+);
+
+bool pmSourcePhotometryAper(
+    float   *apMag,                     ///< aperture flux magnitude
+    pmModel *model,                     ///< model used for photometry
+    psImage *image,                     ///< image pixels to be used
+    psImage *mask                       ///< mask of pixels to ignore
+);
+
+bool pmSourceMagnitudesInit (psMetadata *config);
+bool pmSourceMagnitudes (pmSource *source, pmPSF *psf, pmSourcePhotometryMode mode);
+float pmSourceCrossProduct (pmSource *Mi, pmSource *Mj);
+float pmSourceCrossWeight (pmSource *Mi, pmSource *Mj);
+bool pmSourcePixelWeight (float *pixWeight, pmModel *model, psImage *image, psImage *mask);
+
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: trunk/psModules/src/objects/pmSourceSky.c
===================================================================
--- trunk/psModules/src/objects/pmSourceSky.c	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceSky.c	(revision 6872)
@@ -0,0 +1,120 @@
+/** @file  pmSourceSky.c
+ *
+ *  Functions to measure the local sky and sky variance for sources on images
+ *
+ *  @author GLG, MHPCC
+ *  @author EAM, IfA: significant modifications.
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "pslib.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmPeaks.h"
+#include "pmMoments.h"
+#include "pmModel.h"
+#include "pmSource.h"
+#include "pmSourceSky.h"
+
+/******************************************************************************
+pmSource *pmSourceLocalSky(source, statsOptions, Radius): this
+routine creates a new pmSource.moments element if needed and sets pmSource.pmMoments.sky
+ 
+The sky value is set from the pixels in the square annulus surrounding the
+peak pixel.
+ 
+The source.pixels and source.mask must already exist
+*****************************************************************************/
+
+bool pmSourceLocalSky(
+    pmSource *source,
+    psStatsOptions statsOptions,
+    psF32 Radius)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->pixels, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->mask, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+    PS_ASSERT_INT_NONNEGATIVE(Radius, false);
+
+    psImage *image = source->pixels;
+    psImage *mask  = source->mask;
+    pmPeak *peak  = source->peak;
+    psRegion srcRegion;
+
+    srcRegion = psRegionForSquare(peak->x, peak->y, Radius);
+    srcRegion = psRegionForImage(mask, srcRegion);
+
+    psImageMaskRegion(mask, srcRegion, "OR", PM_SOURCE_MASK_MARKED);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    myStats = psImageStats(myStats, image, mask, 0xff);
+    psImageMaskRegion(mask, srcRegion, "AND", ~PM_SOURCE_MASK_MARKED);
+
+    psF64 tmpF64;
+    p_psGetStatValue(myStats, &tmpF64);
+    psFree(myStats);
+
+    if (isnan(tmpF64)) {
+        psTrace(__func__, 3, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->Sky = (psF32) tmpF64;
+    psTrace(__func__, 3, "---- %s(true) end ----\n", __func__);
+    return (true);
+}
+
+// A complementary function to pmSourceLocalSky: calculate the local median variance
+bool pmSourceLocalSkyVariance(
+    pmSource *source,
+    psStatsOptions statsOptions,
+    psF32 Radius)
+{
+    psTrace(__func__, 3, "---- %s() begin ----\n", __func__);
+    PS_ASSERT_PTR_NON_NULL(source, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->weight, false);
+    PS_ASSERT_IMAGE_NON_NULL(source->mask, false);
+    PS_ASSERT_PTR_NON_NULL(source->peak, false);
+    PS_ASSERT_INT_POSITIVE(Radius, false);
+    PS_ASSERT_INT_NONNEGATIVE(Radius, false);
+
+    psImage *image = source->weight;
+    psImage *mask  = source->mask;
+    pmPeak *peak  = source->peak;
+    psRegion srcRegion;
+
+    srcRegion = psRegionForSquare(peak->x, peak->y, Radius);
+    srcRegion = psRegionForImage(mask, srcRegion);
+
+    psImageMaskRegion(mask, srcRegion, "OR", PM_SOURCE_MASK_MARKED);
+    psStats *myStats = psStatsAlloc(statsOptions);
+    myStats = psImageStats(myStats, image, mask, 0xff);
+    psImageMaskRegion(mask, srcRegion, "AND", ~PM_SOURCE_MASK_MARKED);
+
+    psF64 tmpF64;
+    p_psGetStatValue(myStats, &tmpF64);
+    psFree(myStats);
+
+    if (isnan(tmpF64)) {
+        psTrace(__func__, 3, "---- %s(false) end ----\n", __func__);
+        return(false);
+    }
+    if (source->moments == NULL) {
+        source->moments = pmMomentsAlloc();
+    }
+    source->moments->dSky = (psF32) tmpF64;
+    psTrace(__func__, 3, "---- %s(true) end ----\n", __func__);
+    return (true);
+}
Index: trunk/psModules/src/objects/pmSourceSky.h
===================================================================
--- trunk/psModules/src/objects/pmSourceSky.h	(revision 6872)
+++ trunk/psModules/src/objects/pmSourceSky.h	(revision 6872)
@@ -0,0 +1,42 @@
+/** @file  pmSourceSky.h
+ *
+ *  @author EAM, IfA; GLG, MHPCC
+ *
+ *  @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2006-04-17 18:01:05 $
+ *
+ *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
+ *
+ */
+
+# ifndef PM_SOURCE_SKY_H
+# define PM_SOURCE_SKY_H
+
+/** pmSourceLocalSky()
+ *
+ * Measure the local sky in the vicinity of the given source. The Radius
+ * defines the square aperture in which the moments will be measured. This
+ * function assumes the source pixels have been defined, and that the value of
+ * Radius here is smaller than the value of Radius used to define the pixels. The
+ * annular region not contained within the radius defined here is used to measure
+ * the local background in the vicinity of the source. The local background
+ * measurement uses the specified statistic passed in via the statsOptions entry.
+ * This function allocates the pmMoments structure. The resulting sky is used to
+ * set the value of the pmMoments.sky element of the provided pmSource structure.
+ *
+ */
+bool pmSourceLocalSky(
+    pmSource *source,   ///< The input image (float)
+    psStatsOptions statsOptions, ///< The statistic used in calculating the background sky
+    float Radius   ///< The inner radius of the square annulus to exclude
+);
+
+
+// A complementary function to pmSourceLocalSky: calculate the local sky variance
+bool pmSourceLocalSkyVariance(
+    pmSource *source,   ///< The input image (float)
+    psStatsOptions statsOptions, ///< The statistic used in calculating the background sky
+    float Radius   ///< The inner radius of the square annulus to exclude
+);
+
+# endif /* PM_SOURCE_PHOTOMETRY_H */
Index: trunk/psModules/src/pslib/.cvsignore
===================================================================
--- trunk/psModules/src/pslib/.cvsignore	(revision 6872)
+++ trunk/psModules/src/pslib/.cvsignore	(revision 6872)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: trunk/psModules/src/pslib/Makefile.am
===================================================================
--- trunk/psModules/src/pslib/Makefile.am	(revision 6872)
+++ trunk/psModules/src/pslib/Makefile.am	(revision 6872)
@@ -0,0 +1,28 @@
+noinst_LTLIBRARIES = libpsmodulepslib.la
+
+libpsmodulepslib_la_CPPFLAGS = $(SRCINC) $(PSMODULE_CFLAGS)
+libpsmodulepslib_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulepslib_la_SOURCES  = \
+    psAdditionals.c \
+    psEllipse.c \
+    psImageJpeg.c \
+    psImageFlip.c \
+    psImageUnbin.c \
+    psLine.c \
+    psMetadataItemParse.c \
+    psPolynomialUtils.c \
+    psRegionIsBad.c \
+    psSparse.c
+
+psmoduleincludedir = $(includedir)
+psmoduleinclude_HEADERS = \
+    psAdditionals.h \
+    psEllipse.h \
+    psImageJpeg.h \
+    psImageFlip.h \
+    psImageUnbin.h \
+    psLine.h \
+    psMetadataItemParse.h \
+    psPolynomialUtils.h \
+    psRegionIsBad.h \
+    psSparse.h
Index: trunk/psModules/src/pslib/psAdditionals.c
===================================================================
--- trunk/psModules/src/pslib/psAdditionals.c	(revision 6872)
+++ trunk/psModules/src/pslib/psAdditionals.c	(revision 6872)
@@ -0,0 +1,238 @@
+#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))) {
+        // 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;
+}
+
+// may need to extend this to change the keyname in the copy
+bool psMetadataItemTransfer (psMetadata *out, psMetadata *in, char *key)
+{
+
+    psMetadataItem *item = psMetadataLookup (in, key);
+    if (item == NULL)
+        return false;
+
+    psMetadataAddItem (out, item, PS_LIST_TAIL, PS_META_REPLACE);
+    return true;
+}
+
+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", (char*)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;
+}
+
+// XXX: This should probably be implemented using strpbrk
+psList *psStringSplit(const char *string,
+                      const char *splitters)
+{
+    psList *values = psListAlloc(NULL); // The list of values to return
+    if (string == NULL)
+        return values;  // NULL string is not an error, just an empty list
+
+    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;
+}
+
+psArray *psStringSplitArray (const char *string, const char *splitters)
+{
+
+    psList *list = psStringSplit (string, splitters);
+    psArray *array = psListToArray (list);
+    psFree (list);
+    return array;
+}
+
+#ifndef whitespace
+#define whitespace(c) (((c) == ' ') || ((c) == '\t'))
+#endif
+
+/* Strip whitespace from the start and end of STRING. */
+int psStringStrip (char *string)
+{
+
+    int i;
+
+    if (string == (char *) NULL)
+        return (FALSE);
+
+    for (i = 0; whitespace (string[i]); i++)
+        ;
+    if (i)
+        memmove (string, string + i, strlen(string+i)+1);
+    for (i = strlen (string) - 1; (i > 0) && whitespace (string[i]); i--)
+        ;
+    string[++i] = 0;
+    return (i);
+
+}
+
+bool psFitsWriteHeaderNotImage (psFits *fits, psMetadata *header)
+{
+
+    int status = 0;
+    char fitsErr[1024];
+
+    psMetadataItem *mdi = NULL;
+    mdi = psMetadataLookup (header, "NAXIS");
+    if (mdi != NULL) {
+        mdi->data.S32 = 0;
+        mdi->type = PS_DATA_S32;
+    }
+
+    fits_create_img (fits->fd, SHORT_IMG, 0, NULL, &status);
+
+    if (status != 0) {
+        fits_get_errstatus (status, fitsErr);
+        psError (PS_ERR_IO, true, PS_ERRORTEXT_psFits_WRITE_FAILED, fitsErr);
+        exit (2);
+    }
+
+    psFitsWriteHeader (header, fits);
+
+    return true;
+}
Index: trunk/psModules/src/pslib/psAdditionals.h
===================================================================
--- trunk/psModules/src/pslib/psAdditionals.h	(revision 6872)
+++ trunk/psModules/src/pslib/psAdditionals.h	(revision 6872)
@@ -0,0 +1,42 @@
+// Functions that should go into psLib.
+
+
+#ifndef PS_ADDITIONALS_H
+#define PS_ADDITIONALS_H
+
+#include "pslib.h"
+
+// Deep copy of metadata
+// Corrected version of MHPCC code in psLib at the moment
+psMetadata *pap_psMetadataCopy(psMetadata *out, // Target, to which the copy is made
+                               const psMetadata *in // Source, from which the copy is made
+                              );
+
+// copy a metadata item from one psMetadata to another
+bool psMetadataItemTransfer (psMetadata *out, // Destination: copy is placed here
+                             psMetadata *in,  // Source: item comes from here
+                             char *key        // key to identify the metadata item
+                            );
+
+
+// Print out the metadata
+void psMetadataPrint(psMetadata *md,    // Metadata to print
+                     int level          // Indent level
+                    );
+
+// Split string on given characters
+psList *psStringSplit(const char *string, // String to split
+                      const char *splitters // Characters on which to split
+                     );
+
+// Split string on given characters
+psArray *psStringSplitArray(const char *string, // String to split
+                            const char *splitters // Characters on which to split
+                           );
+
+// strip whitespace from head and tail of string
+int psStringStrip (char *string);
+
+// write out header with NAXIS=0
+bool psFitsWriteHeaderNotImage (psFits *fits, psMetadata *header);
+#endif
Index: trunk/psModules/src/pslib/psEllipse.c
===================================================================
--- trunk/psModules/src/pslib/psEllipse.c	(revision 6872)
+++ trunk/psModules/src/pslib/psEllipse.c	(revision 6872)
@@ -0,0 +1,62 @@
+# include "pslib.h"
+# include "psEllipse.h"
+
+psEllipseAxes psEllipseMomentsToAxes (psEllipseMoments moments)
+{
+
+    psEllipseAxes axes;
+
+    double f = sqrt (0.25*PS_SQR(moments.x2 - moments.y2) + PS_SQR(moments.xy));
+    if (f > (moments.x2 + moments.y2) / 2.0) {
+        f = 0.98*(moments.x2 + moments.y2) / 2.0;
+    }
+
+    axes.major = sqrt (0.5*(moments.x2 + moments.y2) + f);
+    axes.minor = sqrt (0.5*(moments.x2 + moments.y2) - f);
+    axes.theta = atan2 (2*moments.xy, moments.x2 - moments.y2) / 2;
+    // theta in radians
+
+    return (axes);
+}
+
+psEllipseShape psEllipseAxesToShape (psEllipseAxes axes)
+{
+
+    psEllipseShape shape;
+
+    double r1 = 1.0 / PS_SQR(axes.major) + 1.0 / PS_SQR(axes.minor);
+    double r2 = 1.0 / PS_SQR(axes.major) - 1.0 / PS_SQR(axes.minor);
+
+    double sxr = r1 + r2*cos(2*axes.theta);
+    double syr = r1 - r2*cos(2*axes.theta);
+
+    shape.sx = 1.0 / sqrt(sxr);
+    shape.sy = 1.0 / sqrt(syr);
+    shape.sxy = r2*sin(2*axes.theta);
+
+    return (shape);
+}
+
+psEllipseAxes psEllipseShapeToAxes (psEllipseShape shape)
+{
+
+    psEllipseAxes axes;
+
+    double f1 = 1.0 / PS_SQR(shape.sx) + 1.0 / PS_SQR(shape.sy);
+    double f2 = 1.0 / PS_SQR(shape.sx) - 1.0 / PS_SQR(shape.sy);
+
+    // force the axis ratio to be less than 10
+    double r1 = 0.5*0.95*sqrt (PS_SQR(f1) - PS_SQR(f2));
+
+    shape.sxy = PS_MIN(PS_MAX(shape.sxy, -r1), r1);
+
+    axes.theta = atan2 (-2.0*shape.sxy, f2) / 2.0;
+
+    double Ar = 0.25*f1 + 0.25*sqrt(PS_SQR(f2) + 4*PS_SQR(shape.sxy));
+    double Br = 0.25*f1 - 0.25*sqrt(PS_SQR(f2) + 4*PS_SQR(shape.sxy));
+
+    axes.minor = 1.0 / sqrt (Ar);
+    axes.major = 1.0 / sqrt (Br);
+
+    return (axes);
+}
Index: trunk/psModules/src/pslib/psEllipse.h
===================================================================
--- trunk/psModules/src/pslib/psEllipse.h	(revision 6872)
+++ trunk/psModules/src/pslib/psEllipse.h	(revision 6872)
@@ -0,0 +1,30 @@
+// strucures to define elliptical shape parameters
+typedef struct
+{
+    double major;
+    double minor;
+    double theta;
+}
+psEllipseAxes;
+
+typedef struct
+{
+    double x2;
+    double y2;
+    double xy;
+}
+psEllipseMoments;
+
+typedef struct
+{
+    double sx;
+    double sy;
+    double sxy;
+}
+psEllipseShape;
+
+// conversions between elliptical shape representations
+psEllipseAxes psEllipseMomentsToAxes (psEllipseMoments moments);
+psEllipseShape psEllipseAxesToShape (psEllipseAxes axes);
+psEllipseAxes psEllipseShapeToAxes (psEllipseShape shape);
+
Index: trunk/psModules/src/pslib/psImageFlip.c
===================================================================
--- trunk/psModules/src/pslib/psImageFlip.c	(revision 6872)
+++ trunk/psModules/src/pslib/psImageFlip.c	(revision 6872)
@@ -0,0 +1,76 @@
+#include <stdio.h>
+#include "pslib.h"
+
+
+#define FLIP_X_CASE(TYPENAME,TYPE) \
+case TYPENAME: \
+for (int i = 0; i < image->numRows; i++) { \
+    for (int j = 0; j < image->numCols; j++) { \
+        output->data.TYPE[i][j] = image->data.TYPE[i][image->numCols - j - 1]; \
+    } \
+} \
+break;
+
+// Return a flipped image in x
+psImage *psImageFlipX(psImage *image    // Image to flip
+                     )
+{
+    psImage *output = psImageAlloc(image->numCols, image->numRows, image->type.type); // Output image
+    switch(image->type.type) {
+        FLIP_X_CASE(PS_TYPE_U8,  U8);
+        FLIP_X_CASE(PS_TYPE_U16, U16);
+        FLIP_X_CASE(PS_TYPE_U32, U32);
+        FLIP_X_CASE(PS_TYPE_U64, U64);
+        FLIP_X_CASE(PS_TYPE_S8,  S8);
+        FLIP_X_CASE(PS_TYPE_S16, S16);
+        FLIP_X_CASE(PS_TYPE_S32, S32);
+        FLIP_X_CASE(PS_TYPE_S64, S64);
+        FLIP_X_CASE(PS_TYPE_F32, F32);
+        FLIP_X_CASE(PS_TYPE_F64, F64);
+        FLIP_X_CASE(PS_TYPE_C32, C32);
+        FLIP_X_CASE(PS_TYPE_C64, C64);
+    default:
+        psFree(output);
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Unknown type for input image: %x\n", image->type.type);
+        return NULL;
+    }
+
+    return output;
+}
+
+
+#define FLIP_Y_CASE(TYPENAME,TYPE) \
+case TYPENAME: \
+for (int i = 0; i < image->numRows; i++) { \
+    for (int j = 0; j < image->numCols; j++) { \
+        output->data.TYPE[i][j] = image->data.TYPE[image->numRows - i - 1][j]; \
+    } \
+} \
+break;
+
+// Return a flipped image in y
+psImage *psImageFlipY(psImage *image    // Image to flip
+                     )
+{
+    psImage *output = psImageAlloc(image->numCols, image->numRows, image->type.type); // Output image
+    switch(image->type.type) {
+        FLIP_Y_CASE(PS_TYPE_U8,  U8);
+        FLIP_Y_CASE(PS_TYPE_U16, U16);
+        FLIP_Y_CASE(PS_TYPE_U32, U32);
+        FLIP_Y_CASE(PS_TYPE_U64, U64);
+        FLIP_Y_CASE(PS_TYPE_S8,  S8);
+        FLIP_Y_CASE(PS_TYPE_S16, S16);
+        FLIP_Y_CASE(PS_TYPE_S32, S32);
+        FLIP_Y_CASE(PS_TYPE_S64, S64);
+        FLIP_Y_CASE(PS_TYPE_F32, F32);
+        FLIP_Y_CASE(PS_TYPE_F64, F64);
+        FLIP_Y_CASE(PS_TYPE_C32, C32);
+        FLIP_Y_CASE(PS_TYPE_C64, C64);
+    default:
+        psFree(output);
+        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Unknown type for input image: %x\n", image->type.type);
+        return NULL;
+    }
+
+    return output;
+}
Index: trunk/psModules/src/pslib/psImageFlip.h
===================================================================
--- trunk/psModules/src/pslib/psImageFlip.h	(revision 6872)
+++ trunk/psModules/src/pslib/psImageFlip.h	(revision 6872)
@@ -0,0 +1,13 @@
+#ifndef PS_IMAGE_FLIP_H
+#define PS_IMAGE_FLIP_H
+
+#include "pslib.h"
+
+// Return a flipped image in x
+psImage *psImageFlipX(psImage *image    // Image to flip
+                     );
+// Return a flipped image in y
+psImage *psImageFlipY(psImage *image    // Image to flip
+                     );
+
+#endif
Index: trunk/psModules/src/pslib/psImageJpeg.c
===================================================================
--- trunk/psModules/src/pslib/psImageJpeg.c	(revision 6872)
+++ trunk/psModules/src/pslib/psImageJpeg.c	(revision 6872)
@@ -0,0 +1,191 @@
+# include <stdio.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include "jpeglib.h"
+# include "psImageJpeg.h"
+
+static void psImageJpegColormapFree (psImageJpegColormap *map)
+{
+
+    if (map == NULL)
+        return;
+
+    psFree (map->red);
+    psFree (map->green);
+    psFree (map->blue);
+    return;
+}
+
+psImageJpegColormap *psImageJpegColormapAlloc ()
+{
+
+    psImageJpegColormap *map;
+    map = psAlloc (sizeof(psImageJpegColormap));
+    psMemSetDeallocator(map, (psFreeFunc) psImageJpegColormapFree);
+
+    map->red   = psVectorAlloc (256, PS_TYPE_U8);
+    map->blue  = psVectorAlloc (256, PS_TYPE_U8);
+    map->green = psVectorAlloc (256, PS_TYPE_U8);
+
+    return (map);
+}
+
+psImageJpegColormap *psImageJpegColormapSet (psImageJpegColormap *map, char *name)
+{
+
+    if (map == NULL) {
+        map = psImageJpegColormapAlloc ();
+    }
+
+    /* grayscale */
+    if ((!strcasecmp (name, "grayscale")) || (!strcasecmp (name, "greyscale"))) {
+        for (int i = 0; i < map->red->n; i++) {
+            map->red->data.U8[i]   = PS_JPEG_RANGELIM(i);
+            map->green->data.U8[i] = PS_JPEG_RANGELIM(i);
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(i);
+        }
+        return map;
+    }
+
+    /* -grayscale */
+    if ((!strcasecmp (name, "-grayscale")) || (!strcasecmp (name, "-greyscale"))) {
+        for (int i = 0; i < map->red->n; i++) {
+            map->red->data.U8[i]   = PS_JPEG_RANGELIM(256 - i);
+            map->green->data.U8[i] = PS_JPEG_RANGELIM(256 - i);
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(256 - i);
+        }
+        return map;
+    }
+
+    /* rainbow */
+    if (!strcasecmp (name, "rainbow")) {
+        int I1 = 0.25*map->red->n;
+        int I2 = 0.50*map->red->n;
+        int I3 = 0.75*map->red->n;
+        for (int i = 0; i < I1; i++) {
+            map->red->data.U8[i]   = 0;
+            map->green->data.U8[i] = 0;
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(4*i);
+        }
+        for (int i = I1; i < I2; i++) {
+            map->red->data.U8[i]   = PS_JPEG_RANGELIM(4*(i - I1));
+            map->green->data.U8[i] = 0;
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(4*(I2 - i));
+        }
+        for (int i = I2; i < I3; i++) {
+            map->red->data.U8[i]   = 255;
+            map->green->data.U8[i] = 4*(i - I2);
+            map->blue->data.U8[i]  = 0;
+        }
+        for (int i = I3; i < map->red->n; i++) {
+            map->red->data.U8[i]   = 255;
+            map->green->data.U8[i] = 255;
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(4*(i - I3));
+        }
+        return map;
+    }
+
+    /* heat */
+    if (!strcasecmp (name, "heat")) {
+        int I1 = 0.25*map->red->n;
+        int I2 = 0.50*map->red->n;
+        int I3 = 0.75*map->red->n;
+        for (int i = 0; i < I1; i++) {
+            map->red->data.U8[i]   = PS_JPEG_RANGELIM(2*i);
+            map->green->data.U8[i] = 0;
+            map->blue->data.U8[i]  = 0;
+        }
+        for (int i = I1; i < I2; i++) {
+            map->red->data.U8[i]   = PS_JPEG_RANGELIM(2*i);
+            map->green->data.U8[i] = PS_JPEG_RANGELIM(2*(i - I1));
+            map->blue->data.U8[i]  = 0;
+        }
+        for (int i = I2; i < I3; i++) {
+            map->red->data.U8[i]   = 255;
+            map->green->data.U8[i] = PS_JPEG_RANGELIM(2*(i - I1));
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(2*(i - I2));
+        }
+        for (int i = I3; i < map->red->n; i++) {
+            map->red->data.U8[i]   = 255;
+            map->green->data.U8[i] = 255;
+            map->blue->data.U8[i]  = PS_JPEG_RANGELIM(2*(i - I2));
+        }
+        return map;
+    }
+
+    // invalid colormap : warn user
+    map = psImageJpegColormapSet (map, "greyscale");
+    return map;
+}
+
+bool psImageJpeg (psImageJpegColormap *map, psImage *image, char *filename, float min, float max)
+{
+
+    struct jpeg_compress_struct cinfo;
+    struct jpeg_error_mgr jerr;
+
+    int pixel;
+    JSAMPLE *jpegLine;   // Points to data for current line
+    JSAMPROW jpegLineList[1];  // pointer to JSAMPLE row[s]
+    JSAMPLE *outPix;
+
+    if (map->red == NULL)
+        return false;
+    if (map->green == NULL)
+        return false;
+    if (map->blue == NULL)
+        return false;
+
+    /* JPEG init calls */
+    cinfo.err = jpeg_std_error (&jerr);
+    jpeg_create_compress (&cinfo);
+
+    /* open file, prep for jpeg */
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+        fprintf (stderr, "failed to open %s for output\n", filename);
+        return (TRUE);
+    }
+    jpeg_stdio_dest(&cinfo, f);
+
+    /* set up color jpeg buffers */
+    int quality = 75;
+    cinfo.image_width = image->numCols; // image width and height, in pixels
+    cinfo.image_height = image->numRows;
+    cinfo.input_components = 3;
+    cinfo.in_color_space = JCS_RGB;
+    jpeg_set_defaults (&cinfo);
+    jpeg_set_quality (&cinfo, quality, true); // limit to baseline-JPEG values
+    jpeg_start_compress (&cinfo, true);
+
+    jpegLine = psAlloc (3*image->numCols*sizeof(JSAMPLE));
+    jpegLineList[0] = jpegLine;
+
+    psU8 *Rpix = map->red->data.U8;
+    psU8 *Gpix = map->green->data.U8;
+    psU8 *Bpix = map->blue->data.U8;
+
+    float zero = min;
+    float scale = 256.0/(max - min);
+
+    for (int j = 0; j < image->numRows; j++) {
+        psF32 *row = image->data.F32[j];
+
+        outPix = jpegLine;
+        for (int i = 0; i < image->numCols; i++, outPix += 3) {
+            pixel = PS_JPEG_SCALEVALUE(row[i],zero,scale);
+            outPix[0] = Rpix[pixel];
+            outPix[1] = Gpix[pixel];
+            outPix[2] = Bpix[pixel];
+        }
+        jpeg_write_scanlines (&cinfo, jpegLineList, 1);
+    }
+
+    jpeg_finish_compress (&cinfo);
+    fclose (f);
+    jpeg_destroy_compress (&cinfo);
+
+    psFree (jpegLine);
+    return true;
+}
Index: trunk/psModules/src/pslib/psImageJpeg.h
===================================================================
--- trunk/psModules/src/pslib/psImageJpeg.h	(revision 6872)
+++ trunk/psModules/src/pslib/psImageJpeg.h	(revision 6872)
@@ -0,0 +1,32 @@
+/** @file  psImageJpeg.h
+ *
+ * the functions to generate JPEG images from psImage 
+ */
+
+# ifndef PS_IMAGE_JPEG_H
+# define PS_IMAGE_JPEG_H
+
+typedef struct
+{
+    psString name;
+    psVector *red;
+    psVector *green;
+    psVector *blue;
+}
+psImageJpegColormap;
+
+# define PS_JPEG_RANGELIM(A)(PS_MAX(0,PS_MIN(255,(A))))
+
+# define PS_JPEG_SCALEVALUE(VALUE,ZERO,SCALE)(PS_MAX(0,PS_MIN(255,(SCALE*(VALUE-ZERO)))))
+
+// allocate a colormap (does not define the map values)
+psImageJpegColormap *psImageJpegColormapAlloc ();
+
+// set the colormap values using the supplied name
+psImageJpegColormap *psImageJpegColormapSet (psImageJpegColormap *map, char *name);
+
+// write out a JPEG file using the supplied image and colormap
+// output goes to the specified filename
+bool psImageJpeg (psImageJpegColormap *map, psImage *image, char *filename, float min, float max);
+
+# endif /* PS_IMAGE_JPEG_H */
Index: trunk/psModules/src/pslib/psImageUnbin.c
===================================================================
--- trunk/psModules/src/pslib/psImageUnbin.c	(revision 6872)
+++ trunk/psModules/src/pslib/psImageUnbin.c	(revision 6872)
@@ -0,0 +1,273 @@
+#include <stdio.h>
+#include "pslib.h"
+#include "psImageUnbin.h"
+
+// interpolate from model to background (~bresenham linear interpolation)
+// XXX this code skips the initial pixels?
+psImage *psImageUnbin (psImage *out, psImage *in, int DX, int DY, int dx, int dy)
+{
+
+    int nx = in->numCols;
+    int ny = in->numRows;
+
+    if (out == NULL)
+        psAbort ("psImageUnbin", "please supply output image");
+
+    int Nx = out->numCols;
+    int Ny = out->numRows;
+
+    psF32 **vIn = in->data.F32;
+    psF32 **vOut = out->data.F32;
+
+    for (int Iy = 0; Iy < ny-1; Iy ++) {
+        for (int Ix = 0; Ix < nx-1; Ix ++) {
+
+            float V00 = vIn[Iy+0][Ix+0];
+            float V01 = vIn[Iy+0][Ix+1];
+            float V10 = vIn[Iy+1][Ix+0];
+            float V11 = vIn[Iy+1][Ix+1];
+
+            // a single binned pixel quad
+            // (Xs,Ys) : (Xe,Ye) : binned pixel centers in unbinned coords
+            // corresponding to (Ix,Iy), (Ix+1,Iy+1)
+            int Xs = (Ix + 1)*DX - dx;
+            int Ys = (Iy + 1)*DY - dy;
+            int Xe = Xs + DX;
+            int Ye = Ys + DY;
+
+            for (int iy = Ys; (iy < Ye) && (iy < Ny); iy++) {
+                float Vxs = (V10 - V00)*(iy - Ys) / DY + V00;
+                float Vxe = (V11 - V01)*(iy - Ys) / DY + V01;
+                float dV = (Vxe - Vxs) / DX;
+                float V  = Vxs;
+                for (int ix = Xs; (ix < Xe) && (ix < Nx); ix++) {
+                    vOut[iy][ix] = V;
+                    V += dV;
+                }
+            }
+        }
+    }
+
+    // side pixels
+    int Xs = DX - dx;
+    int Xe = nx*DX - dx;
+    for (int Iy = 0; Iy < ny - 1; Iy++) {
+
+        int Ys = (Iy + 1)*DY - dy;
+        int Ye = Ys + DY;
+
+        // leading edge
+        float V0 = vIn[Iy+0][0];
+        float V1 = vIn[Iy+1][0];
+        float dV = (V1 - V0) / DY;
+        float V = V0;
+        for (int iy = Ys; (iy < Ye) && (iy < Ny); iy++) {
+            for (int ix = 0; ix < Xs; ix++) {
+                vOut[iy][ix] = V;
+            }
+            V += dV;
+        }
+
+        // trailing edge
+        V0 = vIn[Iy+0][nx-1];
+        V1 = vIn[Iy+1][nx-1];
+        dV = (V1 - V0) / DY;
+        V = V0;
+        for (int iy = Ys; (iy < Ye) && (iy < Ny); iy++) {
+            for (int ix = Xe; ix < Nx; ix++) {
+                vOut[iy][ix] = V;
+            }
+            V += dV;
+        }
+    }
+
+    // top and bottom pixels
+    int Ys = DY - dy;
+    int Ye = ny*DY - dy;
+    for (int Ix = 0; Ix < nx - 1; Ix++) {
+
+        int Xs = (Ix + 1)*DX - dx;
+        int Xe = Xs + DX;
+
+        // top edge
+        float V0 = vIn[0][Ix+0];
+        float V1 = vIn[0][Ix+1];
+        float dV = (V1 - V0) / DX;
+        for (int iy = 0; iy < Ys; iy++) {
+            float V = V0;
+            for (int ix = Xs; (ix < Xe) && (ix < Nx); ix++) {
+                vOut[iy][ix] = V;
+                V += dV;
+            }
+        }
+
+        // bottom edge
+        V0 = vIn[ny-1][Ix+0];
+        V1 = vIn[ny-1][Ix+1];
+        dV = (V1 - V0) / DX;
+        for (int iy = Ye; iy < Ny; iy++) {
+            float V = V0;
+            for (int ix = Xs; (ix < Xe) && (ix < Nx); ix++) {
+                vOut[iy][ix] = V;
+                V += dV;
+            }
+        }
+    }
+
+    // the four corners
+    {
+        float V;
+        // 0,0
+        V = vIn[0][0];
+        for (int iy = 0; iy < DY - dy; iy++)
+        {
+            for (int ix = 0; ix < DX - dx; ix++) {
+                vOut[iy][ix] = V;
+            }
+        }
+        // Nx,0
+        V = vIn[0][nx-1];
+        for (int iy = 0; iy < DY - dy; iy++)
+        {
+            for (int ix = nx*DX - dx; ix < Nx; ix++) {
+                vOut[iy][ix] = V;
+            }
+        }
+        // 0,Ny
+        V = vIn[ny-1][0];
+        for (int iy = ny*DY - dy; iy < Ny; iy++)
+        {
+            for (int ix = 0; ix < DX - dx; ix++) {
+                vOut[iy][ix] = V;
+            }
+        }
+        // Nx,Ny
+        V = vIn[nx-1][ny-1];
+        for (int iy = ny*DY - dy; iy < Ny; iy++)
+        {
+            for (int ix = nx*DX - dx; ix < Nx; ix++) {
+                vOut[iy][ix] = V;
+            }
+        }
+    }
+
+    return out;
+}
+
+// random number seed to select a fraction of the image pixels
+static psRandom *rnd = NULL;
+
+// use no more than MAX_SAMPLE_PIXELS pixels for each median box
+static int MAX_SAMPLE_PIXELS;
+
+void psImageClippedStatsInit (int nMax)
+{
+
+    MAX_SAMPLE_PIXELS = nMax;
+
+    if (!rnd) {
+        rnd = psRandomAlloc (PS_RANDOM_TAUS, 0);
+    }
+    return;
+}
+
+void psImageClippedStatsCleanup ()
+{
+    psFree (rnd);
+    rnd = NULL;
+    return;
+}
+
+psStats *psImageClippedStats (psImage *image, psImage *mask, psU8 maskValue, double fmin, double fmax)
+{
+    double value;
+    int nx = image->numCols;
+    int ny = image->numRows;
+
+    psStats *stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_QUARTILE);
+    if (nx*ny <= 0)
+        return stats;
+
+    int Nsubset = PS_MIN (MAX_SAMPLE_PIXELS, nx*ny);
+    int Npixels = nx*ny;
+
+    psVector *values = psVectorAlloc (Nsubset, PS_TYPE_F32);
+
+    float min = values->data.F32[0];
+    float max = values->data.F32[0];
+
+    int n = 0;
+    for (int i = 0; i < Nsubset; i++) {
+        double frnd = psRandomUniform (rnd);
+        int pixel = Npixels * frnd;
+        int ix = pixel % nx;
+        int iy = pixel / nx;
+
+        if (mask && mask->data.U8[iy][ix] & maskValue)
+            continue;
+
+        value = image->data.F32[iy][ix];
+        min = PS_MIN (value, min);
+        max = PS_MIN (value, max);
+        values->data.F32[n] = value;
+        n++;
+    }
+    values->n = n;
+
+    int imin = fmin * n;
+    int imax = fmax * n;
+    int npts = imax - imin + 1;
+
+    fsort (values->data.F32, n);
+
+    value = 0;
+    for (int i = imin; (i <= imax) && (i < n); i++) {
+        value += values->data.F32[i];
+    }
+    value = value / npts;
+
+    stats->robustMedian = value;
+    stats->robustUQ = values->data.F32[imax];
+    stats->robustLQ = values->data.F32[imin];
+
+    psFree (values);
+    return stats;
+    // XXX correct for selection bias??
+}
+
+void fsort (float *value, int N)
+{
+
+    int l,j,ir,i;
+    float temp;
+
+    if (N < 2)
+        return;
+    l = N >> 1;
+    ir = N - 1;
+    for (;;) {
+        if (l > 0) {
+            temp = value[--l];
+        } else {
+            temp = value[ir];
+            value[ir] = value[0];
+            if (--ir == 0) {
+                value[0] = temp;
+                return;
+            }
+        }
+        i = l;
+        j = (l << 1) + 1;
+        while (j <= ir) {
+            if (j < ir && value[j] < value[j+1])
+                ++j;
+            if (temp < value[j]) {
+                value[i]=value[j];
+                j += (i=j) + 1;
+            } else
+                j = ir + 1;
+        }
+        value[i] = temp;
+    }
+}
+
Index: trunk/psModules/src/pslib/psImageUnbin.h
===================================================================
--- trunk/psModules/src/pslib/psImageUnbin.h	(revision 6872)
+++ trunk/psModules/src/pslib/psImageUnbin.h	(revision 6872)
@@ -0,0 +1,18 @@
+#ifndef PS_IMAGE_UNBIN_H
+#define PS_IMAGE_UNBIN_H
+
+#include "pslib.h"
+
+// This needs to be considered more carefully
+psImage *psImageUnbin (psImage *out, psImage *in, int DX, int DY, int dx, int dy);
+
+void psImageClippedStatsInit (int nMax);
+void psImageClippedStatsCleanup ();
+
+// my temporary image stats function; seems to be much faster than psLib???
+psStats *psImageClippedStats (psImage *image, psImage *mask, psU8 maskValue, double fmin, double fmax);
+
+// my temporary sort which is based on N.R.; seems to be faster than psLib sort???
+void fsort (float *value, int N);
+
+#endif
Index: trunk/psModules/src/pslib/psLine.c
===================================================================
--- trunk/psModules/src/pslib/psLine.c	(revision 6872)
+++ trunk/psModules/src/pslib/psLine.c	(revision 6872)
@@ -0,0 +1,55 @@
+# include <pslib.h>
+# include "psLine.h"
+
+static void psLineFree (psLine *line)
+{
+
+    if (line == NULL)
+        return;
+
+    psFree (line->line);
+    return;
+}
+
+// allocate a psLine structrue
+psLine *psLineAlloc (int Nline)
+{
+
+    psLine *line;
+    line = psAlloc (sizeof(psLine));
+    psMemSetDeallocator(line, (psFreeFunc) psLineFree);
+
+    line->Nline = 0;
+    line->NLINE = Nline;
+    line->line = psAlloc (Nline);
+    return (line);
+}
+
+bool psLineInit (psLine *line)
+{
+    if (line == NULL)
+        return (false);
+    line->Nline = 0;
+    return (true);
+}
+
+bool psLineAdd (psLine *line, char *format, ...)
+{
+
+    int Nchar;
+    va_list ap;
+
+    if (line == NULL)
+        return (false);
+
+    int nMax = line->NLINE - line->Nline;
+
+    va_start (ap, format);
+    Nchar = vsnprintf (&line->line[line->Nline], nMax, format, ap);
+    line->Nline += PS_MIN (nMax - 1, Nchar);
+    va_end (ap);
+
+    if (Nchar >= nMax)
+        return (false);
+    return (true);
+}
Index: trunk/psModules/src/pslib/psLine.h
===================================================================
--- trunk/psModules/src/pslib/psLine.h	(revision 6872)
+++ trunk/psModules/src/pslib/psLine.h	(revision 6872)
@@ -0,0 +1,27 @@
+/** @file  psLine.h
+ *
+ * the psLine functions allow manipulation of fixed-length lines
+ */
+
+# ifndef PS_LINE_H
+# define PS_LINE_H
+
+// structure to carry a dynamic string
+typedef struct
+{
+    int NLINE;
+    int Nline;
+    char *line;
+}
+psLine;
+
+// allocate a line object of length Nline
+psLine      *psLineAlloc (int Nline);
+
+// (re-)init the line
+bool      psLineInit (psLine *line);
+
+// add the new string segment to the line
+bool      psLineAdd (psLine *line, char *format, ...);
+
+# endif /* PS_LINE_H */
Index: trunk/psModules/src/pslib/psMetadataItemParse.c
===================================================================
--- trunk/psModules/src/pslib/psMetadataItemParse.c	(revision 6872)
+++ trunk/psModules/src/pslib/psMetadataItemParse.c	(revision 6872)
@@ -0,0 +1,85 @@
+#include <stdio.h>
+#include "pslib.h"
+
+psF32 psMetadataItemParseF32(psMetadataItem *item
+                            )
+{
+    switch (item->type) {
+    case PS_DATA_F32:
+        return item->data.F32;
+    case PS_DATA_F64:
+        // Assume it's OK to truncate to floating point from double
+        return (float)item->data.F64;
+    case PS_DATA_S32:
+        // Promote to float
+        return (float)item->data.S32;
+    default:
+        psError(PS_ERR_IO, true, "Item %s (%s) is not of floating point type (%x) --- treating as NaN.\n",
+                item->name, item->comment, item->type);
+        return NAN;
+    }
+}
+
+psF64 psMetadataItemParseF64(psMetadataItem *item
+                            )
+{
+    switch (item->type) {
+    case PS_TYPE_F64:
+        return item->data.F64;
+    case PS_TYPE_F32:
+        // Promote to double
+        return (double)item->data.F32;
+    case PS_TYPE_S32:
+        // Promote to double
+        return (double)item->data.S32;
+    default:
+        psError(PS_ERR_IO, true, "Item %s (%s) is not of double-precision floating point type (%x) "
+                "--- treating as NaN.\n", item->name, item->comment, item->type);
+        return NAN;
+    }
+}
+
+psS32 psMetadataItemParseS32(psMetadataItem *item
+                            )
+{
+    switch (item->type) {
+    case PS_TYPE_S32:
+        return item->data.S32;
+    case PS_TYPE_F32:
+        psLogMsg(__func__, PS_LOG_WARN, "Item %s (%s) should be S32, but is F32 --- converting.\n",
+                 item->name, item->comment);
+        return (int)item->data.F32;
+    case PS_TYPE_F64:
+        psLogMsg(__func__, PS_LOG_WARN, "Item %s (%s) should be S32, but is F64 --- converting.\n",
+                 item->name, item->comment);
+        return (int)item->data.F64;
+    default:
+        psError(PS_ERR_IO, true, "Item %s (%s) is not of integer type (%x) --- treating as zero.\n",
+                item->name, item->comment, item->type);
+        return 0;
+    }
+}
+
+psString psMetadataItemParseString(psMetadataItem *item
+                                  )
+{
+    switch (item->type) {
+    case PS_DATA_STRING:
+        return psMemIncrRefCounter(item->data.V);
+    case PS_DATA_F32: {
+            psString value = NULL;    // String to return
+            psStringAppend(&value, "%f", item->data.F32);
+            return value;
+        }
+    case PS_DATA_S32: {
+            psString value = NULL;    // String to return
+            psStringAppend(&value, "%d", item->data.S32);
+            return value;
+        }
+    default:
+        psError(PS_ERR_IO, true, "Item %s (%s) is not of string type (%x) --- treating as "
+                "undefined.\n", item->name, item->comment, item->type);
+        return psStringCopy("");
+    }
+}
+
Index: trunk/psModules/src/pslib/psMetadataItemParse.h
===================================================================
--- trunk/psModules/src/pslib/psMetadataItemParse.h	(revision 6872)
+++ trunk/psModules/src/pslib/psMetadataItemParse.h	(revision 6872)
@@ -0,0 +1,15 @@
+#ifndef PS_METADATA_ITEM_PARSE_H
+#define PS_METADATA_ITEM_PARSE_H
+
+#include "pslib.h"
+
+psF32 psMetadataItemParseF32(psMetadataItem *item
+                            );
+psF64 psMetadataItemParseF64(psMetadataItem *item
+                            );
+psS32 psMetadataItemParseS32(psMetadataItem *item
+                            );
+psString psMetadataItemParseString(psMetadataItem *item
+                                  );
+
+#endif
Index: trunk/psModules/src/pslib/psPolynomialUtils.c
===================================================================
--- trunk/psModules/src/pslib/psPolynomialUtils.c	(revision 6872)
+++ trunk/psModules/src/pslib/psPolynomialUtils.c	(revision 6872)
@@ -0,0 +1,161 @@
+# include <stdio.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include "psPolynomialUtils.h"
+
+psPolynomial4D *psVectorChiClipFitPolynomial4D(
+    psPolynomial4D *poly,
+    psStats *stats,
+    const psVector *mask,
+    psMaskType maskValue,
+    const psVector *f,
+    const psVector *fErr,
+    const psVector *x,
+    const psVector *y,
+    const psVector *z,
+    const psVector *t)
+{
+    PS_ASSERT_POLY_NON_NULL(poly, NULL);
+    PS_ASSERT_POLY_TYPE(poly, PS_POLYNOMIAL_ORD, NULL);
+    PS_ASSERT_PTR_NON_NULL(stats, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(f, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(f, NULL);
+    if (mask != NULL) {
+        PS_ASSERT_VECTORS_SIZE_EQUAL(f, mask, NULL);
+        PS_ASSERT_VECTOR_TYPE(mask, PS_TYPE_U8, NULL);
+    }
+    PS_ASSERT_VECTOR_NON_NULL(x, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(f, x, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(x, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(y, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(f, y, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(y, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(z, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(f, z, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(z, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(t, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(f, t, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(t, NULL);
+    PS_ASSERT_VECTOR_NON_NULL(fErr, NULL);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(fErr, mask, NULL);
+    PS_ASSERT_VECTOR_TYPE_F32_OR_F64(fErr, NULL);
+
+    // clipping range defined by min and max and/or clipSigma
+    float minClipSigma;
+    float maxClipSigma;
+    if (isfinite(stats->max)) {
+        maxClipSigma = +fabs(stats->max);
+    } else {
+        maxClipSigma = +fabs(stats->clipSigma);
+    }
+    if (isfinite(stats->min)) {
+        minClipSigma = -fabs(stats->min);
+    } else {
+        minClipSigma = -fabs(stats->clipSigma);
+    }
+    psVector *fit   = NULL;
+    psVector *resid = psVectorAlloc (x->n, PS_TYPE_F64);
+
+    // eventual expansion: user supplies one of various stats option pairs,
+    // eg (SAMPLE_MEAN | SAMPLE_STDEV) and the correct pair is used to
+    // evaluate the clipping sigma
+    // for now, for the SAMPLE_MEDIAN and SAMPLE_STDEV to be used
+    stats->options |= (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+
+    for (int N = 0; N < stats->clipIter; N++) {
+        int Nkeep = 0;
+
+        poly = psVectorFitPolynomial4D (poly, mask, maskValue, f, fErr, x, y, z, t);
+        fit = psPolynomial4DEvalVector (poly, x, y, z, t);
+        resid = (psVector *) psBinaryOp (resid, (void *) f, "-", (void *) fit);
+
+        stats  = psVectorStats (stats, resid, NULL, mask, maskValue);
+        psTrace (__func__, 5, "resid stats: %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+
+        // set mask if pts are not valid
+        // we are masking out any point which is out of range
+        // recovery is not allowed with this scheme
+        for (int i = 0; i < resid->n; i++) {
+            if ((mask != NULL) && (mask->data.U8[i] & maskValue)) {
+                continue;
+            }
+            float sigma = hypot (psVectorGet (fErr, i), stats->sampleStdev);
+            if (resid->data.F64[i] - stats->sampleMedian > sigma*maxClipSigma) {
+                if (mask != NULL) {
+                    mask->data.U8[i] |= 0x01;
+                }
+                continue;
+            }
+            if (resid->data.F64[i] - stats->sampleMedian < sigma*minClipSigma) {
+                if (mask != NULL) {
+                    mask->data.U8[i] |= 0x01;
+                }
+                continue;
+            }
+            Nkeep ++;
+        }
+
+        psTrace (__func__, 4, "keeping %d of %d pts for fit\n",
+                 Nkeep, x->n);
+
+        stats->clippedNvalues = Nkeep;
+        psFree (fit);
+    }
+    // Free local temporary variables
+    psFree (resid);
+
+    if (poly == NULL) {
+        psError(PS_ERR_UNKNOWN, true, "Could not fit a polynomial to the data.  Returning NULL.\n");
+        return(NULL);
+    }
+    return(poly);
+}
+
+psPolynomial2D *psImageBicubeFit (psImage *image, int x, int y)
+{
+
+    int ix = x - image->col0;
+    int iy = y - image->row0;
+
+    psF32 *Fm = &image->data.F32[iy - 1][ix];
+    psF32 *Fo = &image->data.F32[iy + 0][ix];
+    psF32 *Fp = &image->data.F32[iy + 1][ix];
+
+    double Fxm = Fm[-1] + Fo[-1] + Fp[-1];
+    double Fxp = Fm[+1] + Fo[+1] + Fp[+1];
+    double Fym = Fm[-1] + Fm[+0] + Fm[+1];
+    double Fyp = Fp[-1] + Fp[+0] + Fp[+1];
+    double Foo = Fym + Fyp + Fo[-1] + Fo[+0] + Fo[+1];
+
+    psPolynomial2D *poly = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, 2, 2);
+    poly->mask[2][2] = 1;
+    poly->mask[1][2] = 1;
+    poly->mask[2][1] = 1;
+
+    poly->coeff[0][0] = Foo*(5.0/9.0) - (Fxp + Fxm)/3.0 - (Fyp + Fym)/3.0 ;
+
+    poly->coeff[1][0] = (Fxp - Fxm)/6.0;
+    poly->coeff[0][1] = (Fyp - Fym)/6.0;
+
+    poly->coeff[2][0] = (Fxp + Fxm)/2.0 - Foo/3.0;
+    poly->coeff[0][2] = (Fyp + Fym)/2.0 - Foo/3.0;
+
+    poly->coeff[1][1] = (Fp[+1] + Fm[-1] - Fm[+1] - Fp[-1])/4.0;
+
+    return (poly);
+}
+
+psPlane psImageBicubeMin (psPolynomial2D *poly)
+{
+
+    psPlane min;
+
+    min.xErr = min.yErr = 0;
+
+    double det = 4*poly->coeff[2][0]*poly->coeff[0][2] - PS_SQR(poly->coeff[1][1]);
+
+    min.x = (poly->coeff[1][1]*poly->coeff[0][1] - 2*poly->coeff[0][2]*poly->coeff[1][0]) / det;
+    min.y = (poly->coeff[1][1]*poly->coeff[1][0] - 2*poly->coeff[2][0]*poly->coeff[0][1]) / det;
+    return (min);
+}
Index: trunk/psModules/src/pslib/psPolynomialUtils.h
===================================================================
--- trunk/psModules/src/pslib/psPolynomialUtils.h	(revision 6872)
+++ trunk/psModules/src/pslib/psPolynomialUtils.h	(revision 6872)
@@ -0,0 +1,28 @@
+/** @file  psPolynomialUtils.h
+ *
+ * extra psPolynomial-related functions
+ */
+
+# ifndef PS_POLY_UTILS_H
+# define PS_POLY_UTILS_H
+
+// perform vector clip-fit based on significance of deviations
+psPolynomial4D *psVectorChiClipFitPolynomial4D(
+    psPolynomial4D *poly,
+    psStats *stats,
+    const psVector *mask,
+    psMaskType maskValue,
+    const psVector *f,
+    const psVector *fErr,
+    const psVector *x,
+    const psVector *y,
+    const psVector *z,
+    const psVector *t);
+
+// fit a 2D 2nd order polynomial to the 9 pixels centered on (x,y)
+psPolynomial2D *psImageBicubeFit (psImage *image, int x, int y);
+
+// detemine the min(max) of the special 2D 2nd order polynomial
+psPlane psImageBicubeMin (psPolynomial2D *poly);
+
+# endif /* PS_POLY_UTILS_H */
Index: trunk/psModules/src/pslib/psRegionIsBad.c
===================================================================
--- trunk/psModules/src/pslib/psRegionIsBad.c	(revision 6872)
+++ trunk/psModules/src/pslib/psRegionIsBad.c	(revision 6872)
@@ -0,0 +1,21 @@
+#include <stdio.h>
+#include "pslib.h"
+
+bool psRegionIsBad(const psRegion region)
+{
+    return isnan(region.x0) || isnan(region.x1) || isnan(region.y0) || isnan(region.y1);
+}
+
+bool psRegionIsNaN (psRegion region)
+{
+
+    if (!isfinite(region.x0))
+        return true;
+    if (!isfinite(region.x1))
+        return true;
+    if (!isfinite(region.y0))
+        return true;
+    if (!isfinite(region.y1))
+        return true;
+    return false;
+}
Index: trunk/psModules/src/pslib/psRegionIsBad.h
===================================================================
--- trunk/psModules/src/pslib/psRegionIsBad.h	(revision 6872)
+++ trunk/psModules/src/pslib/psRegionIsBad.h	(revision 6872)
@@ -0,0 +1,12 @@
+#ifndef PS_REGION_IS_BAD_H
+#define PS_REGION_IS_BAD_H
+
+#include "pslib.h"
+
+bool psRegionIsBad(const psRegion region);
+
+// test is any element of the region is not finite
+bool psRegionIsNaN (psRegion region // test for this region
+                   );
+
+#endif
Index: trunk/psModules/src/pslib/psSparse.c
===================================================================
--- trunk/psModules/src/pslib/psSparse.c	(revision 6872)
+++ trunk/psModules/src/pslib/psSparse.c	(revision 6872)
@@ -0,0 +1,245 @@
+# include <pslib.h>
+# include "psSparse.h"
+
+static void psSparseFree (psSparse *sparse)
+{
+    if (sparse == NULL)
+        return;
+    psFree (sparse->Aij);
+    psFree (sparse->Bfj);
+    psFree (sparse->Qii);
+    psFree (sparse->Si);
+    psFree (sparse->Sj);
+    return;
+}
+
+// allocate a sparse matrix container for Nrows, with Nelem slots allocated
+psSparse *psSparseAlloc (int Nrows, int Nelem)
+{
+
+    psSparse *sparse = (psSparse *) psAlloc (sizeof(psSparse));
+    sparse->Aij = psVectorAlloc (Nelem, PS_DATA_F32);
+    sparse->Si  = psVectorAlloc (Nelem, PS_DATA_S32);
+    sparse->Sj  = psVectorAlloc (Nelem, PS_DATA_S32);
+
+    sparse->Aij->n = 0;
+    sparse->Si->n  = 0;
+    sparse->Sj->n  = 0;
+    sparse->Nelem = 0;
+
+    sparse->Bfj = psVectorAlloc (Nrows, PS_DATA_F32);
+    sparse->Qii = psVectorAlloc (Nrows, PS_DATA_F32);
+
+    sparse->Nrows = Nrows;
+    psMemSetDeallocator(sparse, (psFreeFunc) psSparseFree);
+    return (sparse);
+}
+
+// user should only add elements above the diagonal, but we don't check this
+void psSparseMatrixElement (psSparse *sparse, int i, int j, float value)
+{
+
+    int k;
+
+    if (i < j) {
+        fprintf (stderr, "*** error: subdiagonal element ***\n");
+        return;
+    }
+
+    if (i == j) {
+        // add to the diagonal
+        sparse->Qii->data.F32[i] = value;
+
+        // check vectors lengths and extend if needed
+        if (sparse->Nelem >= sparse->Aij->nalloc) {
+            psVectorRealloc (sparse->Aij, sparse->Aij->nalloc + 100);
+            psVectorRealloc (sparse->Si,  sparse->Si->nalloc + 100);
+            psVectorRealloc (sparse->Sj,  sparse->Sj->nalloc + 100);
+        }
+
+        k = sparse->Nelem;
+        sparse->Aij->data.F32[k] = value;
+        sparse->Si->data.S32[k]  = i;
+        sparse->Sj->data.S32[k]  = j;
+
+        sparse->Nelem ++;
+        sparse->Aij->n ++;
+        sparse->Si->n ++;
+        sparse->Sj->n ++;
+    } else {
+        // check vectors lengths and extend if needed
+        if (sparse->Nelem >= sparse->Aij->nalloc - 1) {
+            psVectorRealloc (sparse->Aij, sparse->Aij->nalloc + 100);
+            psVectorRealloc (sparse->Si,  sparse->Si->nalloc + 100);
+            psVectorRealloc (sparse->Sj,  sparse->Sj->nalloc + 100);
+        }
+
+        k = sparse->Nelem;
+        sparse->Aij->data.F32[k] = value;
+        sparse->Si->data.S32[k]  = i;
+        sparse->Sj->data.S32[k]  = j;
+        k++;
+
+        sparse->Aij->data.F32[k] = value;
+        sparse->Si->data.S32[k]  = j;
+        sparse->Sj->data.S32[k]  = i;
+
+        sparse->Nelem  += 2;
+        sparse->Aij->n += 2;
+        sparse->Si->n  += 2;
+        sparse->Sj->n  += 2;
+    }
+    return;
+}
+
+void psSparseVectorElement (psSparse *sparse, int i, float value)
+{
+
+    sparse->Bfj->data.F32[i] = value;
+    return;
+}
+
+// multiple A * x
+psVector *psSparseMatrixTimesVector (psVector *output, psSparse *matrix, psVector *vector)
+{
+
+    int i, Nelem;
+    float F;
+
+    if (output == NULL) {
+        output = psVectorAlloc (vector->n, PS_DATA_F32);
+    }
+
+    Nelem = 0;
+    for (int j = 0; j < vector->n; j++) {
+        F = 0;
+        while (matrix->Sj->data.S32[Nelem] == j) {
+            i = matrix->Si->data.S32[Nelem];
+            F += vector->data.F32[i] * matrix->Aij->data.F32[Nelem];
+            Nelem++;
+        }
+        output->data.F32[j] = F;
+    }
+    return (output);
+}
+
+psVector *psSparseSolve (psVector *guess, psSparseConstraint constraint, psSparse *sparse, int Niter)
+{
+
+    psF32 dG;
+
+    psVector *Qii = sparse->Qii;
+    psVector *Bfj = sparse->Bfj;
+
+    guess = psVectorCopy (guess, Bfj, PS_DATA_F32);
+
+    // temporary storage for intermediate results
+    psVector *dQ = psVectorAlloc (guess->n, PS_DATA_F32);
+
+    for (int j = 0; j < Niter; j++) {
+        dQ = psSparseMatrixTimesVector (dQ, sparse, guess);
+        for (int i = 0; i < dQ->n; i++) {
+            dG = (dQ->data.F32[i] - Bfj->data.F32[i]) / Qii->data.F32[i];
+            if (fabs (dG) > constraint.paramDelta) {
+                if (dG > 0) {
+                    dG = +constraint.paramDelta;
+                } else {
+                    dG = -constraint.paramDelta;
+                }
+            }
+            guess->data.F32[i] -= dG;
+            guess->data.F32[i] = PS_MAX (guess->data.F32[i], constraint.paramMin);
+            guess->data.F32[i] = PS_MIN (guess->data.F32[i], constraint.paramMax);
+        }
+    }
+    psFree (dQ);
+    return (guess);
+}
+
+void psSparseResort (psSparse *sparse)
+{
+
+    int Nelem = sparse->Nelem;
+
+    psVector *index = psVectorSortIndex (NULL, sparse->Sj);
+    psVector *Aij = sparse->Aij;
+    psVector *Si = sparse->Si;
+    psVector *Sj = sparse->Sj;
+
+    // allocate new temporary vectors
+    psVector *tAij = psVectorAlloc (Nelem, PS_DATA_F32);
+    psVector *tSi  = psVectorAlloc (Nelem, PS_DATA_S32);
+    psVector *tSj  = psVectorAlloc (Nelem, PS_DATA_S32);
+    for (int i = 0; i < Nelem; i++) {
+        int j = index->data.U32[i];
+        tAij->data.F32[i] = Aij->data.F32[j];
+        tSi->data.S32[i]  = Si->data.S32[j];
+        tSj->data.S32[i]  = Sj->data.S32[j];
+    }
+    psFree (index);
+    psFree (Aij);
+    psFree (Si);
+    psFree (Sj);
+
+    sparse->Aij = tAij;
+    sparse->Si = tSi;
+    sparse->Sj = tSj;
+    return;
+}
+
+void psSparseMatrixTest ()
+{
+
+    // build a sparse matrix
+    psSparse *sparse = psSparseAlloc (3, 9);
+
+    psSparseMatrixElement (sparse, 0, 0, 3.0);
+    psSparseMatrixElement (sparse, 1, 1, 2.0);
+    psSparseMatrixElement (sparse, 2, 2, 1.0);
+
+    psSparseMatrixElement (sparse, 1, 0, 0.1);
+    psSparseMatrixElement (sparse, 2, 0, -0.1);
+
+    psSparseResort (sparse);
+    for (int i = 0; i < sparse->Nelem; i++) {
+        fprintf (stderr, "%d %d %f\n",
+                 sparse->Si->data.S32[i],
+                 sparse->Sj->data.S32[i],
+                 sparse->Aij->data.F32[i]);
+    }
+
+    psVector *x = psVectorAlloc (3, PS_DATA_F32);
+    x->data.F32[0] = 3;
+    x->data.F32[1] = 5;
+    x->data.F32[2] = 7;
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+
+    psVector *B = psSparseMatrixTimesVector (NULL, sparse, x);
+    fprintf (stderr, "B: %f %f %f\n", B->data.F32[0], B->data.F32[1], B->data.F32[2]);
+
+    sparse->Bfj->data.F32[0] = B->data.F32[0];
+    sparse->Bfj->data.F32[1] = B->data.F32[1];
+    sparse->Bfj->data.F32[2] = B->data.F32[2];
+
+    psSparseConstraint constraint;
+    constraint.paramMin   = -1e8;
+    constraint.paramMax   = +1e8;
+    constraint.paramDelta = +1e8;
+
+    x = psSparseSolve (x, constraint, sparse, 0);
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+
+    x = psSparseSolve (x, constraint, sparse, 1);
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+
+    x = psSparseSolve (x, constraint, sparse, 2);
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+
+    x = psSparseSolve (x, constraint, sparse, 3);
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+
+    x = psSparseSolve (x, constraint, sparse, 4);
+    fprintf (stderr, "x: %f %f %f\n", x->data.F32[0], x->data.F32[1], x->data.F32[2]);
+    return;
+}
+
Index: trunk/psModules/src/pslib/psSparse.h
===================================================================
--- trunk/psModules/src/pslib/psSparse.h	(revision 6872)
+++ trunk/psModules/src/pslib/psSparse.h	(revision 6872)
@@ -0,0 +1,62 @@
+
+/** @file  psSparse.h
+ *
+ * functions to manipulate sparse matrices equations
+ *  
+ */
+
+# ifndef PS_SPARSE_H
+# define PS_SPARSE_H
+
+// constraints to limit the range of the matrix equation solution
+typedef struct
+{
+    double paramDelta;
+    double paramMin;
+    double paramMax;
+}
+psSparseConstraint;
+
+// a sparse matrix equation: A x = Bf
+// Aij contains the populated elements of the matrix
+// Bfj contains the elements of the vector Bf
+// Qii contains the diagonal elements of Aij
+// Si contains the i-index values of Aij
+// Sj contains the j-index values of Aij
+typedef struct
+{
+    psVector *Aij;
+    psVector *Bfj;
+    psVector *Qii;
+    psVector *Si;
+    psVector *Sj;
+    int Nelem;
+    int Nrows;
+}
+psSparse;
+
+// allocate a sparse matrix structure
+psSparse *psSparseAlloc (int Nrows, int Nelem);
+
+// add a new matrix element
+// user should only add elements above the diagonal
+void psSparseMatrixElement (psSparse *sparse, int i, int j, float value);
+
+// define a new sparse matrix equation vector element
+void psSparseVectorElement (psSparse *sparse, int i, float value);
+
+// perform the operation matrix * vector on a sparse matrix and a vector
+psVector *psSparseMatrixTimesVector (psVector *output, psSparse *matrix, psVector *vector);
+
+// re-sort a sparse matrix to have all elements in index order rather than insertion order
+// call this before solving, but after populating matrix and vector
+void psSparseResort (psSparse *sparse);
+
+// solve the equation A x = Bf for the value of x
+// a good starting guess is the vector Bf
+psVector *psSparseSolve (psVector *guess, psSparseConstraint constraint, psSparse *sparse, int Niter);
+
+// test of the sparse matrix solutions (move to test suite)
+void psSparseMatrixTest ();
+
+# endif /* PS_SPARSE_H */
Index: trunk/psModules/src/psmodules.h
===================================================================
--- trunk/psModules/src/psmodules.h	(revision 6872)
+++ trunk/psModules/src/psmodules.h	(revision 6872)
@@ -0,0 +1,75 @@
+#ifndef PS_MODULES_H
+#define PS_MODULES_H
+
+// XXX should this be included explicitly?
+#include <pslib.h>
+
+// the following headers are from psModule:pslib
+#include <psImageJpeg.h>
+#include <psImageFlip.h>
+#include <psImageUnbin.h>
+#include <psLine.h>
+#include <psPolynomialUtils.h>
+#include <psSparse.h>
+#include <psEllipse.h>
+#include <psAdditionals.h>
+#include <psRegionIsBad.h>
+#include <psMetadataItemParse.h>
+
+// the following headers are from psModule:config
+#include <pmConfig.h>
+
+// the following headers are from psModule:astrom
+#include <pmConcepts.h>
+#include <pmConceptsRead.h>
+#include <pmConceptsStandard.h>
+#include <pmConceptsWrite.h>
+#include <pmHDU.h>
+#include <pmHDUUtils.h>
+#include <pmFPA.h>
+#include <pmFPAview.h>
+#include <pmFPAfile.h>
+#include <pmFPARead.h>
+#include <pmFPAConstruct.h>
+#include <pmFPACopy.h>
+#include <pmFPARead.h>
+#include <pmFPAWrite.h>
+#include <pmFPA_JPEG.h>
+
+#include <pmReadout.h>
+// #include <pmChipMosaic.h>
+// #include <pmFPAAstrometry.h>
+
+// the following headers are from psModule:detrend
+#include <pmFlatField.h>
+#include <pmFlatFieldErrors.h>
+#include <pmMaskBadPixels.h>
+#include <pmMaskBadPixelsErrors.h>
+#include <pmNonLinear.h>
+
+// the following headers are from psModule:imcombine
+#include <pmImageCombine.h>
+// #include <pmReadoutCombine.h>
+
+// the following headers are from psModule:detrend
+#include <pmSubtractBias.h>
+#include <pmImageSubtract.h>
+// #include <pmSubtractSky.h>
+
+// the following headers are from psModule:objects
+# include <pmPeaks.h>
+# include <pmMoments.h>
+# include <pmModel.h>
+# include <pmSource.h>
+# include <pmSourceIO.h>
+# include <pmSourceSky.h>
+# include <pmSourceFitModel.h>
+# include <pmSourceContour.h>
+# include <pmGrowthCurve.h>
+# include <pmPSF.h>
+# include <pmPSF_IO.h>
+# include <pmPSFtry.h>
+# include <pmModelGroup.h>
+# include <pmSourcePhotometry.h>
+
+#endif
Index: trunk/psModules/test/pslib/.cvsignore
===================================================================
--- trunk/psModules/test/pslib/.cvsignore	(revision 6872)
+++ trunk/psModules/test/pslib/.cvsignore	(revision 6872)
@@ -0,0 +1,5 @@
+.deps
+.libs
+Makefile
+Makefile.in
+temp
Index: trunk/psModules/test/pslib/Makefile.am
===================================================================
--- trunk/psModules/test/pslib/Makefile.am	(revision 6872)
+++ trunk/psModules/test/pslib/Makefile.am	(revision 6872)
@@ -0,0 +1,16 @@
+# Makefile for psModule tests
+
+AM_LDFLAGS = -L$(top_builddir)/src -lpsmodule $(PSMODULE_LIBS)
+AM_CFLAGS  = @AM_CFLAGS@ $(PSMODULE_CFLAGS) $(SRCINC)
+
+TESTS =
+
+check_PROGRAMS = $(TESTS)
+
+TESTS_ENVIRONMENT = perl $(top_srcdir)/test/runTest --verified=$(srcdir)/verified
+
+tests: $(TESTS)
+
+CLEANFILES = $(TESTS) temp/*
+
+test: check
