Index: /branches/eam_branch_20090208/psLib/src/fits/psFits.c
===================================================================
--- /branches/eam_branch_20090208/psLib/src/fits/psFits.c	(revision 21432)
+++ /branches/eam_branch_20090208/psLib/src/fits/psFits.c	(revision 21432)
@@ -0,0 +1,941 @@
+/** @file  psFits.c
+ *
+ *  @brief Contains Fits I/O routines
+ *
+ *  @ingroup FileIO
+ *
+ *  @author Robert DeSonia, MHPCC
+ *
+ *  @version $Revision: 1.85 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-10 20:58:17 $
+ *
+ *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <libgen.h>
+
+#include "psFits.h"
+#include "psFitsHeader.h"
+#include "psError.h"
+#include "psAssert.h"
+#include "psImageStructManip.h"
+#include "psMemory.h"
+#include "psString.h"
+#include "psLogMsg.h"
+#include "psTrace.h"
+#include "psVector.h"
+#include "psAbort.h"
+#include "psFitsFloat.h"
+
+#define MAX_STRING_LENGTH 256           // Maximum length string for FITS routines
+
+#define FITS_OPEN_RETRIES 16            // Number of retries to attempt when opening a FITS file
+#define FITS_OPEN_RETRY_WAIT_MIN 200000 // Wait between retries (usec) first time
+#define FITS_OPEN_RETRY_WAIT_MAX 5000000 // double wait time up to 5 sec
+
+static char *defaultExtword = "EXTNAME";
+
+
+bool p_psFitsDumpErrors (const char* filename, unsigned int lineno, const char* func,
+                         psErrorCode code, const char *message, ...) {
+
+    char fitsErr[MAX_STRING_LENGTH];
+
+    va_list ap;
+    va_start(ap, message);
+    p_psErrorV(filename, lineno, func, PS_ERR_BAD_FITS, true, message, ap);
+    va_end(ap);
+
+    while (fits_read_errmsg(fitsErr)) {
+        p_psError(filename, lineno, func, PS_ERR_BAD_FITS, false, "CFITSIO error: %s", fitsErr);
+    }
+
+    return true;
+}
+
+psErrorCode p_psFitsError(const char* filename, unsigned int lineno, const char* func,
+                          int status, bool new, const char *errorMsg, ...)
+{
+    char fitsErr[MAX_STRING_LENGTH];
+
+    if (status == 0) {
+        return PS_ERR_NONE;
+    }
+
+    va_list ap;                         // Variable arguments
+    va_start(ap, errorMsg);
+    psErrorV(PS_ERR_IO, new, errorMsg, ap);
+    va_end(ap);
+
+    while (fits_read_errmsg(fitsErr)) {
+        psError(PS_ERR_IO, false, "[CFITSIO error: %s]", fitsErr);
+    }
+    return PS_ERR_IO;
+}
+
+static bool isHDUEmpty(const psFits* fits)
+{
+    /* check for keys - no keys means this is really an empty HDU */
+    int keysexist = -1;
+    int morekeys;
+    int status = 0;
+
+    fits_get_hdrspace(fits->fd, &keysexist, &morekeys, &status);
+
+    // if no keys exist and not primary HDU, this really is an empty HDU
+    if (keysexist == 0) {
+        return true;
+    }
+
+    return false;
+
+}
+
+static bool fitsClose(psFits* fits)
+{
+    int status = 0;
+
+    if (fits != NULL) {
+        if (fits_close_file(fits->fd, &status)) {
+            psFitsDumpErrors (PS_ERR_IO, "Error while closing psFits object");
+            return false;
+        }
+        fits->fd = NULL;
+    }
+    return true;
+}
+
+static void fitsFree(psFits* fits)
+{
+    if (!fits) return;
+    if (fits->fd) {
+        fitsClose(fits);
+    }
+    psFree(fits->options);
+}
+
+bool psFitsClose(psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    bool status = fitsClose(fits);
+    psFree(fits);
+
+    return status;
+}
+
+psFits* psFitsOpen(const char* name, const char* mode)
+{
+    PS_ASSERT_STRING_NON_EMPTY(name, NULL);
+
+    // Check that the directory exists: for NFS-mounted file systems, the directory may
+    // take some time to appear
+
+    int dirstat = 0;                    // Status of directory access
+    psString tmpname = psStringCopy(name); // Copy of filename, since dirname() may modify
+    const char *dir = dirname(tmpname); // Directory for file
+    useconds_t waittime = FITS_OPEN_RETRY_WAIT_MIN; // Wait time between retries (usec)
+
+    for (int i = 0; (i < FITS_OPEN_RETRIES) && ((dirstat = access(dir, F_OK)) != 0); i++) {
+        if (errno != ENOENT) {
+            // The error is more serious than NFS-mount delays
+            int thisErrno = errno;      // Error number
+            char errorBuf[MAX_STRING_LENGTH], *errorMsg;
+#if ((_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE)
+            errorMsg = strerror_r(thisErrno, errorBuf, MAX_STRING_LENGTH);
+#else
+            strerror_r(thisErrno, errorBuf, MAX_STRING_LENGTH);
+            errorMsg = errorBuf;
+#endif
+            psError(PS_ERR_IO, true, "Directory (%s) for requested file is not accessible: %s",
+                    dir, errorMsg);
+
+            psFree(tmpname);
+            return NULL;
+        }
+
+        usleep(waittime);
+        // double waittime until we get to the max value
+        if (waittime < FITS_OPEN_RETRY_WAIT_MAX) {
+            waittime *= 2;
+        }
+    }
+    if (dirstat != 0) {
+        psError(PS_ERR_IO, true, "Directory (%s) for requested file is not accessible, timed out", dir);
+        psFree(tmpname);
+        return NULL;
+    }
+    psFree(tmpname);
+
+    /* check the mode to determine how to open/create file */
+    int iomode;
+    bool newFile;
+    if (strcmp(mode,"r") == 0) {
+        iomode = READONLY;
+        newFile = false;
+    } else if (strcmp(mode,"rw") == 0 || strcmp(mode,"r+") == 0) {
+        iomode = READWRITE;
+        newFile = false;
+    } else if (strcmp(mode,"w") == 0 || strcmp(mode,"w+") == 0) {
+        iomode = READWRITE;
+        newFile = true;
+    } else if (strcmp(mode,"a") == 0|| strcmp(mode,"a+") == 0) {
+        iomode = READWRITE;
+        newFile = (access(name, F_OK) != 0);
+    } else {
+        // mode is not valid
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Specified mode, '%s', is invalid.  Supported modes are r, r+, rw, w, w+, a, or a+.",
+                mode);
+        return NULL;
+    }
+
+    int status = 0;                     // CFITSIO status
+    fitsfile *fptr = NULL;              // Pointer to the FITS file
+
+    if (newFile) {
+        /* Check if an existing file is in the way before creating file */
+        if (access(name, F_OK) == 0) {
+            // file exists, delete old one first
+            if (remove(name)) {
+                int thisErrno = errno;
+                char errorBuf[64], *errorMsg;
+# if ((_POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600) && ! _GNU_SOURCE)
+                errorMsg = strerror_r (errno, errorBuf, 64);
+# else
+                strerror_r (errno, errorBuf, 64);
+                errorMsg = errorBuf;
+# endif
+                psError (PS_ERR_IO, true, "Failed to delete a previously-existing file: %s", errorMsg);
+                fprintf (stderr, "errno: %d, %s, %s : %lx, %lx\n", thisErrno, errorMsg, errorBuf, (long int) errorMsg, (long int) errorBuf);
+                return NULL;
+            }
+        }
+        if (!access(name, F_OK)) {
+            psError (PS_ERR_IO, true, "deleted file still exists!");
+            return NULL;
+        }
+
+        #if ( CFITSIO_DISKFILE == 1 )
+        (void)fits_create_diskfile
+        #else
+        (void)fits_create_file
+        #endif
+        (&fptr, name, &status);
+        if (fptr == NULL || status != 0) {
+            psFitsDumpErrors (PS_ERR_IO, _("Could not create file,'%s'"), name);
+            return NULL;
+        }
+    } else {
+        #if ( CFITSIO_DISKFILE == 1 )
+        (void)fits_open_diskfile
+        #else
+        (void)fits_open_file
+        #endif
+        (&fptr, name, iomode, &status);
+        if (fptr == NULL || status != 0) {
+            psFitsDumpErrors(PS_ERR_IO, _("Could not open file,'%s'"), name);
+            return NULL;
+        }
+    }
+
+    psFits* fits = psAlloc(sizeof(psFits));
+    psMemSetDeallocator(fits, (psFreeFunc)fitsFree);
+
+    fits->fd = fptr;
+    fits->writable = (iomode == READWRITE);
+
+    fits->options = NULL;
+
+    return fits;
+}
+
+
+static void fitsOptionsFree(psFitsOptions *options)
+{
+    psFree(options->extword);
+}
+
+
+psFitsOptions *psFitsOptionsAlloc(void)
+{
+    psFitsOptions *options = psAlloc(sizeof(psFitsOptions)); // Options, to return
+    psMemSetDeallocator(options, (psFreeFunc)fitsOptionsFree);
+
+    options->extword = NULL;
+
+    options->conventions.compression = true;
+    options->conventions.psBitpix = true;
+
+    options->floatType = PS_FITS_FLOAT_NONE;
+
+    options->bitpix = 0;
+
+    options->scaling = PS_FITS_SCALE_NONE;
+    options->fuzz = true;
+    options->bscale = 1.0;
+    options->bzero = 0.0;
+    options->mean = NAN;
+    options->stdev = NAN;
+    options->stdevBits = 4;
+    options->stdevNum = 5.0;
+
+    return options;
+}
+
+static void psFitsCompressionFree(psFitsCompression *comp)
+{
+    PS_ASSERT_PTR_NON_NULL(comp,);
+    psFree(comp->tilesize);
+}
+
+psFitsCompression* psFitsCompressionAlloc(
+    psFitsCompressionType type,         ///< type of compression
+    psVector *tilesize,                 ///< vector defining compression tile size
+    int noisebits,                      ///< noise bits
+    int scale,                          ///< hcompress scale
+    int smooth                          ///< hcompress smothing
+)
+{
+    psFitsCompression *comp = psAlloc(sizeof(psFitsCompression));
+    psMemSetDeallocator(comp, (psFreeFunc) psFitsCompressionFree);
+
+    comp->type = type;
+    comp->tilesize = psVectorCopy(NULL, tilesize, PS_DATA_S64);
+    comp->noisebits = noisebits;
+    comp->scale = scale;
+    comp->smooth = smooth;
+
+    return comp;
+}
+
+bool psMemCheckFits(psPtr ptr)
+{
+    PS_ASSERT_PTR(ptr, false);
+    return ( psMemGetDeallocator(ptr) == (psFreeFunc)fitsFree );
+}
+
+bool psFitsSetExtnameWord(psFits *fits, const char *extword)
+{
+    PS_ASSERT_PTR_NON_NULL(fits,    false);
+    PS_ASSERT_PTR_NON_NULL(extword, false);
+
+    if (!fits->options) {
+        fits->options = psFitsOptionsAlloc();
+    }
+
+    psFree(fits->options->extword);
+    fits->options->extword = psStringCopy(extword);
+    return true;
+}
+
+// Files compressed with cfitsio's "imcopy" program may have multiple EXTNAME keywords, with the first set to
+// COMPRESSED_IMAGE.  However, fits_movnam_hdu won't find the second (proper) value of EXTNAME, and so can
+// fail to find a perfectly legitimate extension, simply because imcopy does something silly.  However, we
+// really want to be able to read these files (MegaCam data are shipped as imcopy-compressed images).
+// Therefore, we implement our own version of moving to an extension specified by name.  The pure cfitsio
+// version is used if "conventions.compression" handling is turned off in the psFits structure.
+bool psFitsMoveExtName(const psFits* fits,
+                       const char* extname)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_STRING_NON_EMPTY(extname, false);
+
+    int status = 0;
+
+    psFitsOptions *options = fits->options; // FITS options
+    if (options && !options->conventions.compression && !options->extword) {
+        // User wants to use cfitsio.  Good luck to them!
+        if (fits_movnam_hdu(fits->fd, ANY_HDU, (char*)extname, 0, &status) != 0) {
+            psFitsError(status, true, _("Could not find HDU '%s'"), extname);
+            return false;
+        }
+        return true;
+    }
+
+    // Ignore EXTNAME with value COMPRESSED_IMAGE?
+    bool ignoreCI = (!options || (options->conventions.compression && (strcmp(extname, "COMPRESSED_IMAGE") != 0)));
+
+    char *extword = (options && options->extword) ? options->extword : "EXTNAME"; // Word for extension name
+
+#if 0
+    // XXX Future optimisation: loop through from the current HDU to the end, then from the start to the
+    // current position.  This will save seeking through the file multiple times.
+    int currentExt = psFitsGetExtNum(fits); // Current extension number
+    int numExt = psFitsGetSize(fits);   // Total number of extensions
+#endif
+
+    for (int i = 1; true; i++) {
+        int hdutype = 0;
+        if (fits_movabs_hdu(fits->fd, i, &hdutype, &status)) {
+            // We've run off the end
+            psFitsError(status, true, _("Could not find HDU with %s = '%s'"), extword, extname);
+            return false;
+        }
+        // Is there a keyword called 'extword'? (read as string regardless of type)
+        char name[MAX_STRING_LENGTH];  // Name of extension
+        if (fits_read_keyword(fits->fd, extword, name, NULL, &status)) {
+            // It doesn't exist in the header.
+            // This isn't the extension you're looking for.  Move along.
+            status = 0;
+            continue;
+        }
+        char *fixed = p_psFitsHeaderParseString(name); // Parsed version (removing quotes and spaces)
+
+        if (ignoreCI && strcmp(fixed, "COMPRESSED_IMAGE") == 0) {
+            // Read it again, Sam
+            if (fits_read_keyword(fits->fd, extword, name, NULL, &status)) {
+                status = 0;
+                continue;
+            }
+            fixed = p_psFitsHeaderParseString(name);
+        }
+
+        if (strcmp(fixed, extname) == 0) {
+            // We've arrived
+            return true;
+        }
+    }
+    psAbort("Should never reach here.");
+}
+
+bool psFitsMoveExtNum(const psFits* fits,
+                      int extnum,
+                      bool relative)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    int status = 0;
+    int hdutype = 0;
+
+    if (relative) {
+        fits_movrel_hdu(fits->fd, extnum, &hdutype, &status);
+        if (status != 0) {
+            psFitsDumpErrors (PS_ERR_LOCATION_INVALID, _("Could not move %d HDUs from current position"), extnum);
+            return false;
+        }
+    } else {
+        fits_movabs_hdu(fits->fd, extnum+1, &hdutype, &status);
+        if (status != 0) {
+            psFitsDumpErrors (PS_ERR_LOCATION_INVALID, _("Could not move to specified HDU #%d."), extnum);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool psFitsMoveLast(psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+
+    int size = psFitsGetSize(fits);
+    if (size == 0) { // empty file -- no action needed
+        return true;
+    } else {
+        return psFitsMoveExtNum(fits,size-1,false);
+    }
+}
+
+int psFitsGetExtNum(const psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    int hdunum;
+    return fits_get_hdu_num(fits->fd,&hdunum) - 1;
+}
+
+psString psFitsGetExtName(const psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, NULL);
+
+    int status = 0;
+    char name[MAX_STRING_LENGTH];
+
+    psFitsOptions *options = fits->options; // FITS options
+    char *extword = (!options || !options->extword) ? defaultExtword : options->extword;
+
+    if (fits_read_key_str(fits->fd, extword, name, NULL, &status) != 0) {
+        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("Header keyword %s is not found"), extword);
+        return NULL;
+    }
+    return psStringCopy(name);
+}
+
+bool psFitsSetExtName(psFits* fits, const char* name)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_STRING_NON_EMPTY(name, false);
+
+    int status = 0;
+
+    psFitsOptions *options = fits->options; // FITS options
+    char *extword = (!options || !options->extword) ? defaultExtword : options->extword;
+
+    if (fits_update_key_str(fits->fd, extword, (char*)name, NULL, &status) != 0) {
+        psFitsDumpErrors (PS_ERR_IO, _("Could not write data to file %s"), name);
+        return false;
+    }
+
+    return true;
+}
+
+bool psFitsDeleteExtNum(psFits* fits,
+                        int extnum,
+                        bool relative)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+
+    // move to the specified HDU
+    if (!psFitsMoveExtNum(fits, extnum, relative) ) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Failed to delete HDU #%d", extnum);
+        return false;
+    }
+
+    int status = 0;
+
+    // OK, now let's delete the HDU
+    if (fits_delete_hdu(fits->fd, NULL, &status) != 0) {
+        psFitsDumpErrors(PS_ERR_IO, _("Could not write data to file extnum %d"), extnum);
+        return false;
+    }
+
+    return true;
+}
+
+bool psFitsDeleteExtName(psFits* fits,
+                         const char* extname)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+    PS_ASSERT_STRING_NON_EMPTY(extname, false);
+
+    // move to the specified HDU
+    if (! psFitsMoveExtName(fits,extname) ) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, false,
+                "Failed to delete HDU with the name '%s'",
+                extname);
+        return false;
+    }
+
+
+    int status = 0;
+
+    // OK, now let's delete the HDU
+    if (fits_delete_hdu(fits->fd, NULL, &status) != 0) {
+        psFitsDumpErrors(PS_ERR_IO, _("Could not write data to file extname %s"), extname);
+        return false;
+    }
+
+    return true;
+}
+
+int psFitsGetSize(const psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, 0);
+
+    int num = 0;
+    int status = 0;
+
+    if (fits_get_num_hdus(fits->fd, &num, &status) != 0) {
+        psFitsDumpErrors(PS_ERR_LOCATION_INVALID, _("Failed to determine the number of HDUs"));
+        return 0;
+    }
+
+    return num;
+}
+
+psFitsType psFitsGetExtType(const psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, PS_FITS_TYPE_NONE);
+
+    int status = 0;
+    int hdutype = PS_FITS_TYPE_NONE;
+
+    if (fits_get_hdu_type(fits->fd, &hdutype, &status) != 0) {
+        psFitsDumpErrors(PS_ERR_LOCATION_INVALID, _("Failed to determine an HDU type"));
+        return PS_FITS_TYPE_NONE;
+    }
+
+    if (hdutype == PS_FITS_TYPE_IMAGE &&
+            psFitsGetExtNum(fits) > 0 &&
+            isHDUEmpty(fits)) {
+        return PS_FITS_TYPE_ANY;
+    }
+
+    return hdutype;
+}
+
+bool psFitsTruncate(psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+
+    int newEnd = psFitsGetExtNum(fits);
+
+    psFitsMoveLast(fits);
+    int end = psFitsGetExtNum(fits);
+
+    // delete HDUs from end to beginning position + 1;
+    for (int lcv=end;lcv > newEnd; lcv--) {
+        if (! psFitsDeleteExtNum(fits,lcv,false)) {
+            // failed to delete an HDU!?
+            psError(PS_ERR_UNKNOWN, false,
+                    "Failed to truncate file.  HDU #%d out of %d could not be deleted.",
+                    lcv,end);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+bool psFitsSetCompression(
+    psFits* fits,                       ///< psFits object to close
+    psFitsCompressionType type,         ///< type of compression
+    psVector *tilesize,
+    int noisebits,                      ///< noise bits
+    int scale,                          ///< hcompress scale
+    int smooth                          ///< hcompress smothing
+)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, false);
+    PS_ASSERT_FITS_WRITABLE(fits, false);
+
+    // convert psFitsCompressionType to cfitsio compression types
+    int comptype;
+    switch (type) {
+        case PS_FITS_COMPRESS_NONE:
+            comptype = 0x0;
+            break;
+        case PS_FITS_COMPRESS_GZIP:
+            comptype = GZIP_1;
+            break;
+        case PS_FITS_COMPRESS_RICE:
+            comptype = RICE_1;
+            break;
+        case PS_FITS_COMPRESS_HCOMPRESS:
+            comptype = HCOMPRESS_1;
+            break;
+        case PS_FITS_COMPRESS_PLIO:
+            comptype = PLIO_1;
+            break;
+        default:
+            psError(PS_ERR_UNKNOWN, true, "invalid psFitsCompressionType");
+            return false;
+    }
+
+    int status = 0;
+    if (fits_set_compression_type(fits->fd, comptype, &status)) {
+        psFitsDumpErrors(PS_ERR_BAD_FITS, "Error while configuring compression");
+        return false;
+    }
+
+    // if we are setting a trivial compression (NONE), don't bother with the other parameters
+    if (type == PS_FITS_COMPRESS_NONE) {
+        return true;
+    }
+
+    PS_ASSERT_VECTOR_NON_NULL(tilesize, false);
+
+    // convert a psVector into the (long *) array that cfitsio requires
+    psVector *dim = NULL;
+    if (sizeof(long) == sizeof(psS64)) {
+        dim = psVectorCopy(NULL, tilesize, PS_DATA_S64);
+        fits_set_tile_dim(fits->fd, psVectorLength(dim), (long *)dim->data.S64, &status);
+    } else if (sizeof(long) == sizeof(psS32)) {
+        dim = psVectorCopy(NULL, tilesize, PS_DATA_S32);
+        fits_set_tile_dim(fits->fd, psVectorLength(dim), (long *)dim->data.S32, &status);
+    } else {
+        psAbort("can't map (long) type to a psLib type");
+    }
+    psFree(dim);
+    // status check belongs to fits_set_tile_dim() call
+    if (status) {
+        fits_set_compression_type(fits->fd, 0x0, &status);
+        psFitsDumpErrors(PS_ERR_BAD_FITS, "Error while configuring compression");
+        return false;
+    }
+
+    // noise bits are irrelevant (not allowed) for PLIO.  XXX actually, it is the data type
+    // that is the restriction; data must be 32 or 64 bit for noise bits to be valid.
+    if (type != PS_FITS_COMPRESS_PLIO) {
+        if (fits_set_noise_bits(fits->fd, noisebits, &status)) {
+            fits_set_compression_type(fits->fd, 0x0, &status);
+            psFitsDumpErrors(PS_ERR_BAD_FITS, "Error while configuring compression");
+            return false;
+        }
+    }
+
+#if FITS_HCOMP
+    if (fits_set_hcomp_scale(fits->fd, scale, &status)) {
+        fits_set_compression_type(fits->fd, 0x0, &status);
+        psError(PS_ERR_BAD_FITS, status, "Error while configuring compression");
+        return false;
+    }
+    if (fits_set_hcomp_smooth(fits->fd, smooth, &status)) {
+        fits_set_compression_type(fits->fd, 0x0, &status);
+        psError(PS_ERR_BAD_FITS, status, "Error while configuring compression");
+        return false;
+    }
+#endif // FITS_HCOMP
+
+    return true;
+}
+
+psFitsCompression *psFitsCompressionGet(psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, NULL);
+
+    int status = 0;                     // cfitsio status
+
+    psFitsCompressionType type = psFitsCompressionGetType(fits);
+    if (type < 0) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to get compression type.");
+        return NULL;
+    }
+
+    psElemType tileType;                // Type corresponding to "long"
+    if (sizeof(long) == sizeof(psS64)) {
+        tileType = PS_TYPE_S64;
+    } else if (sizeof(long) == sizeof(psS32)) {
+        tileType = PS_TYPE_S32;
+    } else {
+        psAbort("can't map (long) type to a psLib type");
+    }
+
+    psVector *tiles = psVectorAlloc(3, tileType); // Tile sizes
+    if (fits_get_tile_dim(fits->fd, 3, (long*)tiles->data.U8, &status)) {
+        psFitsError(status, true, "Unable to get compression tile sizes.");
+        psFree(tiles);
+        return NULL;
+    }
+
+    int noisebits;                      // Noise bits for compression
+    if (fits_get_noise_bits(fits->fd, &noisebits, &status)) {
+        psFitsError(status, true, "Unable to get compression noise bits.");
+        psFree(tiles);
+        return NULL;
+    }
+
+    int hscale = 0, hsmooth = 0;        // Scaling and smoothing for HCOMPRESS
+
+#if FITS_HCOMP
+    if (fits_get_hcomp_scale(fits->fd, &hscale, &status)) {
+        psFitsError(status, true, "Unable to get HCOMPRESS scaling.");
+        psFree(tiles);
+        return NULL;
+    }
+    if (fits_get_hcomp_smooth(fits->fd, &hsmooth, &status)) {
+        psFitsError(status, true, "Unable to get HCOMPRESS smoothing.");
+        psFree(tiles);
+        return NULL;
+    }
+#endif // FITS_HCOMP
+
+    psFitsCompression *compress = psFitsCompressionAlloc(type, tiles, noisebits, hscale, hsmooth);
+    psFree(tiles);                      // Drop reference
+
+    return compress;
+}
+
+psFitsCompressionType psFitsCompressionGetType(psFits* fits)
+{
+    PS_ASSERT_FITS_NON_NULL(fits, -1);
+
+    int status = 0;                     // cfitsio status
+    int comptype = 0;                   // cfitsio compression type
+    if (fits_get_compression_type(fits->fd, &comptype, &status)) {
+        psFitsError(status, true, "Unable to get compression type.");
+        return -1;
+    }
+
+    psFitsCompressionType type;
+    switch (comptype) {
+      case 0:
+        type = PS_FITS_COMPRESS_NONE;
+        break;
+      case GZIP_1:
+        type = PS_FITS_COMPRESS_GZIP;
+        break;
+      case RICE_1:
+        type = PS_FITS_COMPRESS_RICE;
+        break;
+      case HCOMPRESS_1:
+        type = PS_FITS_COMPRESS_HCOMPRESS;
+        break;
+      case PLIO_1:
+        type = PS_FITS_COMPRESS_PLIO;
+        break;
+      default:
+        psError(PS_ERR_UNKNOWN, true, "cfitsio reports unknown compression type.");
+        return -1;
+    }
+
+    return type;
+}
+
+
+bool psFitsCompressionApply(
+    psFits* fits,                       ///< psFits object to close
+    psFitsCompression *comp             ///< options object
+)
+{
+    return psFitsSetCompression(fits, comp->type, comp->tilesize, comp->noisebits, comp->scale, comp->smooth);
+}
+
+
+psDataType p_psFitsTypeFromCfitsio(int datatype)
+{
+    switch (datatype) {
+    case TBYTE:
+        return PS_TYPE_U8;
+    case TSBYTE:
+        return PS_TYPE_S8;
+    case TSHORT:
+        return PS_TYPE_S16;
+    case TUSHORT:
+        return PS_TYPE_U16;
+    case TLONG:
+        if (sizeof(long) == 8) {
+            return PS_TYPE_S64;
+        }
+        // no break
+    case TINT:
+        return PS_TYPE_S32;
+    case TULONG:
+        if (sizeof(unsigned long) == 8) {
+            return PS_TYPE_U64;
+        }
+        // no break
+    case TUINT:
+        return PS_TYPE_U32;
+    case TLONGLONG:
+        return PS_TYPE_S64;
+    case TFLOAT:
+        return PS_TYPE_F32;
+    case TDOUBLE:
+        return PS_TYPE_F64;
+    case TLOGICAL:
+        return PS_TYPE_BOOL;
+    default:
+        psError(PS_ERR_IO, true,
+                "Unknown FITS datatype, %d.",
+                datatype);
+        return 0;
+    }
+}
+
+bool p_psFitsTypeToCfitsio(psDataType type, int* bitPix, double* bZero, int* dataType)
+{
+    int bitpix;
+    int datatype;
+    double bzero = 0.0;
+
+    switch (type) {
+
+    case PS_TYPE_U8:
+        bitpix = BYTE_IMG;
+        // bzero = -1.0 * INT8_MIN;
+        datatype = TBYTE;
+        break;
+
+    case PS_TYPE_S8:
+        bitpix = BYTE_IMG;
+        bzero = +1.0 * INT8_MIN;
+        datatype = TSBYTE;
+        break;
+
+    case PS_TYPE_U16:
+        bitpix = SHORT_IMG;
+        bzero = -1.0 * INT16_MIN;
+        datatype = TUSHORT;
+        break;
+
+    case PS_TYPE_S16:
+        bitpix = SHORT_IMG;
+        datatype = TSHORT;
+        break;
+
+    case PS_TYPE_U32:
+        bitpix = LONG_IMG;
+        bzero = -1.0 * INT32_MIN;
+        datatype = TUINT;
+        break;
+
+    case PS_TYPE_S32:
+        bitpix = LONG_IMG;
+        datatype = TINT;
+        break;
+
+    case PS_TYPE_F32:
+        bitpix = FLOAT_IMG;
+        datatype = TFLOAT;
+        break;
+
+    case PS_TYPE_F64:
+        bitpix = DOUBLE_IMG;
+        datatype = TDOUBLE;
+        break;
+
+    case PS_DATA_STRING:
+        bitpix = BYTE_IMG;
+        datatype = TSTRING;
+        break;
+
+    case PS_DATA_BOOL:
+        bitpix = BYTE_IMG;
+        datatype = TLOGICAL;
+        break;
+
+    default: {
+            char* typeStr;
+            PS_TYPE_NAME(typeStr,type);
+            psError(PS_ERR_BAD_PARAMETER_TYPE, true,
+                    _("Specified type, %s, is not supported."),
+                    typeStr);
+            return false;
+        }
+    }
+
+    // pass back the requested parameters  (NULL parameters are not set, of course).
+    if (bitPix != NULL) {
+        *bitPix = bitpix;
+    }
+
+    if (dataType != NULL) {
+        *dataType = datatype;
+    }
+
+    if (bZero != NULL) {
+        *bZero = bzero;
+    }
+
+    return true;
+}
+
+
+psFitsCompressionType psFitsCompressionTypeFromString(const char *string)
+{
+    if (!string || strlen(string) == 0) {
+        psWarning("Unable to identify compression type --- none set.");
+        return PS_FITS_COMPRESS_NONE;
+    }
+
+    if (strcmp(string, "NONE") == 0) return PS_FITS_COMPRESS_NONE;
+    if (strcmp(string, "GZIP") == 0) return PS_FITS_COMPRESS_GZIP;
+    if (strcmp(string, "RICE") == 0) return PS_FITS_COMPRESS_RICE;
+    if (strcmp(string, "HCOMPRESS") == 0) return PS_FITS_COMPRESS_HCOMPRESS;
+    if (strcmp(string, "PLIO") == 0) return PS_FITS_COMPRESS_PLIO;
+
+    psWarning("Unable to identify compression type (%s) --- none set.", string);
+    return PS_FITS_COMPRESS_NONE;
+}
Index: /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryObjects.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryObjects.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryObjects.c	(revision 21432)
@@ -0,0 +1,937 @@
+/** @file  pmAstrometryObjects.c
+*
+*  @brief This file defines the basic types for matching objects
+*  based on their astrometry.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.45 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2009-02-09 21:25:20 $
+*
+*  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <unistd.h>   // for unlink
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmFPAfile.h"
+#include "pmAstrometryObjects.h"
+#include "pmKapaPlots.h"
+#include "pmAstrometryVisual.h"
+
+#define PM_ASTROMETRYOBJECTS_DEBUG 1
+
+/******************************************************************************
+pmAstromObjSortByMag(**a, **b): sort by mag (descending)
+
+Is this a private routine?
+Should we do the early asserts?
+ ******************************************************************************/
+int pmAstromObjSortByMag(
+    const void **a,
+    const void **b)
+{
+    if (PM_ASTROMETRYOBJECTS_DEBUG) {
+        PS_ASSERT_PTR_NON_NULL(a, 0);
+        PS_ASSERT_PTR_NON_NULL(*a, 0);
+        PS_ASSERT_PTR_NON_NULL(b, 0);
+        PS_ASSERT_PTR_NON_NULL(*b, 0);
+    }
+
+    pmAstromObj *A = *(pmAstromObj **)a;
+    pmAstromObj *B = *(pmAstromObj **)b;
+
+    psF32 diff = A->Mag - B->Mag;
+    if (diff > FLT_EPSILON) {
+        return (-1);
+    }
+
+    if (diff < FLT_EPSILON) {
+        return (+1);
+    }
+
+    return (0);
+}
+
+/************************************************************************************************************/
+/*
+ * Working routine to match two lists (where x[12] are sorted), given psVectors of their coordinates and the
+ * permutation used to sort in x
+ */
+static psArray *match_lists(const psVector *x1, const psVector *y1, // x/y coordinates of first set of objects
+                            const psVector *x2, const psVector *y2, // x/y   "    "    "  second "   "  "   "
+                            const psVector *sorted1, const psVector *sorted2, // mapping to original order
+                            const double RADIUS) // matching radius
+{
+    psArray *matches = psArrayAllocEmpty(x1->n);
+
+    const double RADIUS_SQR = PS_SQR(RADIUS);
+    double dX, dY, dR;
+
+    int jStart;
+    int i = 0, j = 0;
+    while (i < x1->n && j < x2->n) {
+        dX = x1->data.F64[i] - x2->data.F64[j];
+        if (dX <= -RADIUS) {
+            i++;
+            continue;
+        }
+        if (dX >= +RADIUS) {
+            j++;
+            continue;
+        }
+
+        jStart = j;
+        while (fabs(dX) < RADIUS && j < x2->n) {
+
+            dX = x1->data.F64[i] - x2->data.F64[j];
+            dY = y1->data.F64[i] - y2->data.F64[j];
+            dR = dX*dX + dY*dY;
+
+            if (dR > RADIUS_SQR) {
+                j++;
+                continue;
+            }
+
+            // got a match; add to output list
+            pmAstromMatch *match = pmAstromMatchAlloc (sorted1->data.S32[i], sorted2->data.S32[j]);
+            psArrayAdd (matches, 100, match);
+            psFree (match);
+
+            j++;
+        }
+        j = jStart;
+        i++;
+    }
+    return (matches);
+}
+
+/************************************************************************************************************/
+// macro to generate code for radius match function based on desired member
+// radius is in units of matching member (eg, pixels for chip, microns for FP, etc)
+#define MAKE_ASTROM_RADIUS(FUNC, MEMBER) \
+psArray *FUNC( \
+               const psArray *st1, \
+               const psArray *st2, \
+               double RADIUS) \
+{ \
+    PS_ASSERT_PTR_NON_NULL(st1, NULL); \
+    PS_ASSERT_PTR_NON_NULL(st2, NULL); \
+    \
+    assert(st1->n == 0 || pmAstromObjTest(st1->data[0])); \
+    assert(st2->n == 0 || pmAstromObjTest(st2->data[0])); \
+    \
+    /* sort both lists by X coord; st1 first */ \
+    psVector *x1 = psVectorAlloc(st1->n, PS_TYPE_F64); \
+    for (int i = 0; i < st1->n; i++) { \
+        x1->data.F64[i] = ((pmAstromObj *)st1->data[i])->MEMBER->x; \
+    } \
+    const psVector *sorted1 = psVectorSortIndex(NULL, x1); \
+    assert (sorted1->type.type == PS_TYPE_S32); \
+    \
+    psVector *y1 = psVectorAlloc(st1->n, PS_TYPE_F64); \
+    for (int i = 0; i < st1->n; i++) { \
+        x1->data.F64[i] = ((pmAstromObj *)st1->data[sorted1->data.S32[i]])->MEMBER->x; \
+        y1->data.F64[i] = ((pmAstromObj *)st1->data[sorted1->data.S32[i]])->MEMBER->y; \
+    } \
+    \
+    /* now st2 */ \
+    psVector *x2 = psVectorAlloc(st2->n, PS_TYPE_F64); \
+    for (int i = 0; i < st2->n; i++) { \
+        x2->data.F64[i] = ((pmAstromObj *)st2->data[i])->MEMBER->x; \
+    } \
+    const psVector *sorted2 = psVectorSortIndex(NULL, x2); \
+    \
+    psVector *y2 = psVectorAlloc(st2->n, PS_TYPE_F64); \
+    for (int i = 0; i < st2->n; i++) { \
+        x2->data.F64[i] = ((pmAstromObj *)st2->data[sorted2->data.S32[i]])->MEMBER->x; \
+        y2->data.F64[i] = ((pmAstromObj *)st2->data[sorted2->data.S32[i]])->MEMBER->y; \
+    } \
+    /* Do the work */ \
+    psArray *matches = match_lists(x1, y1, x2, y2, sorted1, sorted2, RADIUS); \
+    \
+    psFree((void *)sorted1); \
+    psFree((void *)sorted2); \
+    psFree(x1); \
+    psFree(y1); \
+    psFree(x2); \
+    psFree(y2); \
+    \
+    psLogMsg (__func__, 3, "radius match: %ld pairs (radius: %f)\n", matches->n, RADIUS); \
+    return (matches); \
+}
+
+/******************************************************************************/
+/*
+ * Match two lists of pmAstromObjs, based on the FP, TP, or chip coordinates
+ */
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatch, FP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchFP, FP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchTP, TP)
+MAKE_ASTROM_RADIUS(pmAstromRadiusMatchChip, chip)
+
+/******************************************************************************
+pmAstromMatchFit(map, raw, ref, match, stats): take two matched star lists
+and fit a psPlaneTransform between them
+ ******************************************************************************/
+pmAstromFitResults *pmAstromMatchFit(
+    psPlaneTransform *map,
+    psArray *raw,
+    psArray *ref,
+    psArray *match,
+    psStats *stats)
+{
+    PS_ASSERT_PTR_NON_NULL(map, NULL);
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(match, NULL);
+    PS_ASSERT_PTR_NON_NULL(stats, NULL);
+
+    // reassign values for clip fit
+    // XXX set wt based on mag error?
+    psVector *X = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *Y = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *x = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *y = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *wt = psVectorAlloc (match->n, PS_TYPE_F32);
+    // take the matched stars, first fit
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *rawStar = raw->data[pair->raw];
+        pmAstromObj *refStar = ref->data[pair->ref];
+
+        X->data.F32[i] = rawStar->chip->x;
+        Y->data.F32[i] = rawStar->chip->y;
+
+        x->data.F32[i] = refStar->FP->x;
+        y->data.F32[i] = refStar->FP->y;
+
+        wt->data.F32[i] = 1.0;
+    }
+
+    // constant errors
+    psVector *mask = psVectorAlloc (match->n, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    // the stats options supplied are used to perform the clip fitting
+    pmAstromFitResults *results = pmAstromFitResultsAlloc();
+    results->xStats = psStatsAlloc (PS_STAT_NONE);
+    results->yStats = psStatsAlloc (PS_STAT_NONE);
+    *results->xStats = *stats;
+    *results->yStats = *stats;
+
+    int nIter = stats->clipIter;
+
+    results->xStats->clipIter = 1;
+    results->yStats->clipIter = 1;
+
+    // fit chip-to-FPA transformation
+    // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each
+    // need to use the stats lookups functions to get the width and center
+    for (int i = 0; i < nIter; i++) {
+        if (!psVectorClipFitPolynomial2D (map->x, results->xStats, mask, 0xff, x, wt, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for x\n");
+            psFree (x);
+            psFree (y);
+            psFree (X);
+            psFree (Y);
+            psFree (wt);
+            psFree (mask);
+
+            return results;
+        }
+        psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", results->xStats->clippedMean, results->xStats->clippedStdev, results->xStats->clippedNvalues, x->n);
+
+        if (!psVectorClipFitPolynomial2D (map->y, results->yStats, mask, 0xff, y, wt, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for y\n");
+            psFree (x);
+            psFree (y);
+            psFree (X);
+            psFree (Y);
+            psFree (wt);
+            psFree (mask);
+
+            return results;
+        }
+        psTrace ("psModules.astrom", 3, "y resid: %f +/- %f (%ld of %ld)\n", results->yStats->clippedMean, results->yStats->clippedStdev, results->yStats->clippedNvalues, y->n);
+    }
+    results->xStats->clipIter = stats->clipIter;
+    results->yStats->clipIter = stats->clipIter;
+
+    psFree (x);
+    psFree (y);
+    psFree (X);
+    psFree (Y);
+    psFree (wt);
+    psFree (mask);
+
+    return (results);
+}
+
+
+/******************************************************************************
+pmAstromRotateObj(old, center, angle, angle): rotate & scale the focal-plane coordinates
+about the center coordinate angle specified in radians
+ ******************************************************************************/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle,
+    double scale)
+{
+    PS_ASSERT_PTR_NON_NULL(old, NULL);
+
+    double X, Y;
+    pmAstromObj *newObj;
+    const pmAstromObj *oldObj;
+
+    psArray *new = psArrayAlloc (old->n);
+    double cs = scale*cos(angle);
+    double sn = scale*sin(angle);
+    double xCenter = center.x;
+    double yCenter = center.y;
+
+    for (int i = 0; i < old->n; i++) {
+
+        oldObj = (pmAstromObj *)old->data[i];
+        newObj = pmAstromObjCopy (oldObj);
+
+        X = oldObj->FP->x - xCenter;
+        Y = oldObj->FP->y - yCenter;
+
+        newObj->FP->x = X*cs + Y*sn + xCenter;
+        newObj->FP->y = Y*cs - X*sn + yCenter;
+
+        new->data[i] = newObj;
+    }
+    return (new);
+}
+
+/******************************************************************************
+pmAstromStatsFree(stats)
+ ******************************************************************************/
+static void pmAstromStatsFree(pmAstromStats *stats)
+{
+    if (stats == NULL)
+        return;
+    return;
+}
+
+/******************************************************************************
+pmAstromStatsAlloc()
+ ******************************************************************************/
+pmAstromStats *pmAstromStatsAlloc(void)
+{
+    pmAstromStats *stats = psAlloc (sizeof(pmAstromStats));
+    psMemSetDeallocator (stats, (psFreeFunc)pmAstromStatsFree);
+
+    //    stats->center = {0, 0, 0, 0};
+    //    stats->offset = {0, 0, 0, 0};
+    stats->angle     = 0.0;
+    stats->scale     = 1.0;
+    stats->minMetric = 0.0;
+    stats->minVar    = 0.0;
+    stats->nMatch    = 0;
+    stats->nTest     = 0;
+    stats->nSigma    = 0;
+
+    return (stats);
+}
+
+/******************************************************************************
+pmAstromFitResultsFree(stats)
+ ******************************************************************************/
+static void pmAstromFitResultsFree(pmAstromFitResults *results)
+{
+    if (results == NULL)
+        return;
+    psFree (results->xStats);
+    psFree (results->yStats);
+    return;
+}
+
+/******************************************************************************
+pmAstromFitResultsAlloc()
+ ******************************************************************************/
+pmAstromFitResults *pmAstromFitResultsAlloc(void)
+{
+    pmAstromFitResults *results = psAlloc (sizeof(pmAstromFitResults));
+    psMemSetDeallocator (results, (psFreeFunc)pmAstromFitResultsFree);
+
+    results->xStats    = NULL;
+    results->yStats    = NULL;
+    results->nMatch    = 0;
+    results->nSigma    = 0;
+
+    return (results);
+}
+
+/******************************************************************************
+astromObjFree(obj)
+ ******************************************************************************/
+static void astromObjFree(pmAstromObj *obj)
+{
+    if (obj == NULL) {
+        return;
+    }
+
+    psFree(obj->pix);
+    psFree(obj->cell);
+    psFree(obj->chip);
+    psFree(obj->FP);
+    psFree(obj->TP);
+    psFree(obj->sky);
+
+    return;
+}
+
+
+/******************************************************************************
+pmAstromObjAlloc()
+ ******************************************************************************/
+pmAstromObj *pmAstromObjAlloc(void)
+{
+    pmAstromObj *obj = psAlloc (sizeof(pmAstromObj));
+    psMemSetDeallocator (obj, (psFreeFunc)astromObjFree);
+
+    obj->pix  = psPlaneAlloc();
+    obj->cell = psPlaneAlloc();
+    obj->chip = psPlaneAlloc();
+    obj->FP   = psPlaneAlloc();
+    obj->TP   = psPlaneAlloc();
+    obj->sky  = psSphereAlloc();
+    obj->Mag  = 0;
+    obj->dMag = 0;
+
+    return (obj);
+}
+
+bool pmAstromObjTest(const psPtr ptr)
+{
+    return (psMemGetDeallocator(ptr) == (psFreeFunc)astromObjFree);
+}
+
+
+
+/******************************************************************************
+pmAstromObjCopy(old)
+ ******************************************************************************/
+pmAstromObj *pmAstromObjCopy(const pmAstromObj *old)
+{
+    PS_ASSERT_PTR_NON_NULL(old, NULL);
+    pmAstromObj *obj = pmAstromObjAlloc();
+
+    *obj->pix  = *old->pix;
+    *obj->cell = *old->cell;
+    *obj->chip = *old->chip;
+    *obj->FP   = *old->FP;
+    *obj->TP   = *old->TP;
+    *obj->sky  = *old->sky;
+    obj->Mag   =  old->Mag;
+    obj->dMag  =  old->dMag;
+
+    return(obj);
+}
+
+
+/******************************************************************************
+ ******************************************************************************/
+static void pmAstromMatchFree (pmAstromMatch *match)
+{
+    if (match == NULL)
+        return;
+    return;
+}
+
+
+/******************************************************************************
+ ******************************************************************************/
+pmAstromMatch *pmAstromMatchAlloc(
+    int raw,
+    int ref)
+{
+    pmAstromMatch *match = psAlloc (sizeof(pmAstromMatch));
+    psMemSetDeallocator(match, (psFreeFunc) pmAstromMatchFree);
+
+    match->raw = raw;
+    match->ref = ref;
+
+    return (match);
+}
+
+
+static double maxOffpix;                // maximum allowed offset between lists, in raw pixels
+static double Scale;                    // grid pixel scale static
+double Offset;                          // deltas to pixels
+/******************************************************************************
+AstromGridBin(*dx, *dy, dX, dY): local function to convert x,y coords to grid
+bins it requires the globals defined above.
+
+ ******************************************************************************/
+static bool AstromGridBin(
+    int *dx,
+    int *dy,
+    double dX,
+    double dY)
+{
+    if (PM_ASTROMETRYOBJECTS_DEBUG) {
+        PS_ASSERT_PTR_NON_NULL(dx, false);
+        PS_ASSERT_PTR_NON_NULL(dy, false);
+    }
+
+    if (!isfinite(dX)) return false;
+    if (!isfinite(dY)) return false;
+
+    if (fabs(dX) > maxOffpix) return false;
+    if (fabs(dY) > maxOffpix) return false;
+
+    *dx = dX / Scale + Offset;
+    *dy = dY / Scale + Offset;
+    return true;
+}
+
+
+/******************************************************************************
+pmAstromGridAngle(raw, ref, config): match the two lists using the binned
+delta-delta max.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridAngle(
+    const psArray *raw,
+    const psArray *ref,
+    const psMetadata *config)
+{
+
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+
+    bool status;
+    int nPix;       // size of matching grid
+    int nPixHalf;   // half-size of matching grid
+    double dX, dY;  // offset between a possible matched pair
+    int iX, iY;     // corresponding grid bin
+
+    const pmAstromObj *ob1, *ob2; // short-cut pointers to the objects
+
+    pmAstromStats *stats = pmAstromStatsAlloc();    // output match statistics
+
+    // max allowed offset in either X or Y directions
+    double gridOffset = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.OFFSET");
+
+    // sampling scale of the grid
+    double gridScale  = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.SCALE");
+
+    // set the static scaling factors
+    nPixHalf = (int)(gridOffset / gridScale + 0.5);  // half-grid
+    nPix = 2*nPixHalf + 1;                           // full grid width
+
+    // these are globals used by p_pmAstromGridBin
+    maxOffpix = gridScale * (nPixHalf + 0.5);            // max offset from true center
+    Offset    = maxOffpix / gridScale;
+    Scale     = gridScale;
+
+    // images used as accumulators for the loop below
+    psImage *gridNP = psImageAlloc (nPix, nPix, PS_TYPE_U32);
+    psImage *gridDX = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridDY = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImage *gridD2 = psImageAlloc (nPix, nPix, PS_TYPE_F32);
+    psImageInit (gridNP, 0);
+    psImageInit (gridDX, 0);
+    psImageInit (gridDY, 0);
+    psImageInit (gridD2, 0);
+
+    // short-cut names for grid images
+    psU32 **NP = gridNP->data.U32;
+    psF32 **DX = gridDX->data.F32;
+    psF32 **DY = gridDY->data.F32;
+    psF32 **D2 = gridD2->data.F32;
+
+
+    // accumulate grids for focal plane (L,M) matches
+    for (int i = 0; i < raw->n; i++) {
+        ob1 = (pmAstromObj *)raw->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x;
+            dY = ob1->FP->y - ob2->FP->y;
+
+            // find bin coordinates for this delta-delta
+            if (!AstromGridBin (&iX, &iY, dX, dY)) {
+                continue; // matched pair is too far offset
+            }
+
+            // accumulate bin stats
+            NP[iY][iX] ++;
+            DX[iY][iX] += dX;
+            DY[iY][iX] += dY;
+            D2[iY][iX] += PS_SQR(dX) + PS_SQR(dY);
+        }
+    }
+
+    // now assess the grid images
+    {
+        double minMetric = 1e10;
+        double minVar = 1e10;
+        int minX = -1;
+        int minY = -1;
+        double metric, var;
+
+        // find the max pixel
+        psStats *imStats = psStatsAlloc (PS_STAT_MAX | PS_STAT_MAX | PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+        if (!psImageStats(imStats, gridNP, NULL, 0)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+            psFree(imStats);
+            psFree(gridNP);
+            psFree(gridDX);
+            psFree(gridDY);
+            psFree(gridD2);
+            psFree(stats);
+            return NULL;
+        }
+
+# if 0
+        char line[16];
+        psFits *fits = psFitsOpen ("grid.image.fits", "w");
+        psFitsWriteImage (fits, NULL, gridNP, 0, NULL);
+        psFitsClose (fits);
+        fprintf (stderr, "wrote grid image, press return to continue\n");
+        fgets (line, 15, stdin);
+# endif
+
+        // only check bins with at least 1/2 of max bin
+        // XXX requiring at least 3 matches in bin
+        int minNpts = PS_MAX (0.5*imStats->max, 5);
+        psTrace("psModule.astrom", 5, "minNpts: %d, min: %d, max: %d, median: %f, stdev: %f", minNpts, (int)(imStats->min), (int)(imStats->max), imStats->sampleMedian, imStats->sampleStdev);
+
+        // find the 'best' bin
+        for (int j = 0; j < gridNP->numRows; j++) {
+            for (int i = 0; i < gridNP->numCols; i++) {
+                if (NP[j][i] < minNpts) continue;
+
+                // this metric emphasizes a narrow peak with lots of sources over one with few.
+                var = fabs((D2[j][i]/NP[j][i]) - PS_SQR(DX[j][i]/NP[j][i]) - PS_SQR(DY[j][i]/NP[j][i]));
+                metric = var / PS_SQR(NP[j][i]) / PS_SQR(NP[j][i]);
+
+                // fprintf (stderr, "try : %f %f (%d pts, %f var, %f met)\n", DX[j][i]/NP[j][i], DY[j][i]/NP[j][i], NP[j][i], var, metric);
+
+                if (metric < minMetric) {
+                    minMetric = metric;
+                    minVar    = var;
+                    minX      = i;
+                    minY      = j;
+                }
+            }
+        }
+
+        // convert the bin to delta-delta
+        if ((minX < 0) || (minY < 0))
+        {
+            // no valid matches found
+            stats->offset.x   = 0;
+            stats->offset.y   = 0;
+            stats->minMetric  = minMetric;
+            stats->minVar     = minVar;
+            stats->nMatch     = 0;
+        } else
+        {
+            stats->offset.x  = DX[minY][minX] / NP[minY][minX];
+            stats->offset.y  = DY[minY][minX] / NP[minY][minX];
+            stats->minMetric = minMetric;
+            stats->minVar    = minVar;
+            stats->nMatch    = NP[minY][minX];
+        }
+
+        // XXX this function is crashing
+        // pmAstromVisualPlotGridMatch(raw, ref, gridNP, stats->offset.x, stats->offset.y, maxOffpix, Scale, Offset);
+
+        psFree (imStats);
+        // XXX EAM : This routine, and pmAstromGridMatch, need to handle failure cases better
+    }
+
+    // sort the NP values and choose
+    psVector *listNP = psVectorAlloc (nPix*nPix, PS_TYPE_U32);
+    int n = 0;
+    for (int i = 0; i < nPix; i++) {
+        for (int j = 0; j < nPix; j++) {
+            listNP->data.U32[n] = gridNP->data.U32[j][i];
+            n++;
+        }
+    }
+    psVector *sort = psVectorSort (NULL, listNP);
+    stats->nTest = sort->data.U32[sort->n - 5];
+    stats->nSigma = (stats->nMatch - stats->nTest) / sqrt(stats->nTest);
+    // XXX this needs a better analysis of the image histogram..
+    // fprintf (stderr, "sigma: nMatch: %d, nTest: %d, nTen: %d\n", stats->nMatch, stats->nTest, sort->data.U32[sort->n - 10]);
+
+
+  psFree (sort);
+    psFree (listNP);
+    psFree (gridNP);
+    psFree (gridDX);
+    psFree (gridDY);
+    psFree (gridD2);
+    return (stats);
+}
+
+
+
+/******************************************************************************
+pmAstromGridMatch(*raw, *ref, *config): match two star lists.
+ ******************************************************************************/
+
+pmAstromStats *pmAstromGridMatch(const psArray *raw,
+                                 const psArray *ref,
+                                 const psMetadata *config)
+{
+    PS_ASSERT_PTR_NON_NULL(raw, NULL);
+    PS_ASSERT_PTR_NON_NULL(ref, NULL);
+    PS_ASSERT_PTR_NON_NULL(config, NULL);
+    bool status;
+    double xMin, xMax, yMin, yMax;
+    const pmAstromObj *obj;
+    psArray *rot;
+
+    pmAstromStats *minStat = pmAstromStatsAlloc ();
+    pmAstromStats *newStat = NULL;
+
+    psPlane center;
+
+    // find center of the raw field (focal-plane coords)
+    xMin = yMin = +1e10;
+    xMax = yMax = -1e10;
+    for (int i = 0; i < raw->n; i++) {
+        obj = (pmAstromObj *)raw->data[i];
+        xMin = PS_MIN (obj->FP->x, xMin);
+        xMax = PS_MAX (obj->FP->x, xMax);
+        yMin = PS_MIN (obj->FP->y, yMin);
+        yMax = PS_MAX (obj->FP->y, yMax);
+    }
+    center.x = 0.5*(xMin + xMax);
+    center.y = 0.5*(yMin + yMax);
+
+    double minScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.SCALE");
+    double maxScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MAX.SCALE");
+    double delScale = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.DEL.SCALE");
+
+    double minAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.ANGLE");
+    double maxAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MAX.ANGLE");
+    double delAngle = PS_RAD_DEG*psMetadataLookupF32 (&status, config, "PSASTRO.GRID.DEL.ANGLE");
+    double minSigma = psMetadataLookupF32 (&status, config, "PSASTRO.GRID.MIN.SIGMA");
+
+    minStat->minMetric = 1e10;
+    for (double scale = minScale; scale <= maxScale; scale += delScale) {
+        for (double angle = minAngle; angle <= maxAngle; angle += delAngle) {
+            rot = pmAstromRotateObj (raw, center, angle, scale);
+
+# if 0
+            FILE *f1 = fopen ("raw.dat", "w");
+            for (int i = 0; i < rot->n; i++) {
+                pmAstromObj *obj = rot->data[i];
+                fprintf (f1, "%8.2f %8.2f   %6.2f\n", obj->FP->x, obj->FP->y, obj->Mag);
+            }
+            fclose (f1);
+            FILE *f2 = fopen ("ref.dat", "w");
+            for (int i = 0; i < ref->n; i++) {
+                pmAstromObj *obj = ref->data[i];
+                fprintf (f2, "%8.2f %8.2f   %6.2f\n", obj->FP->x, obj->FP->y, obj->Mag);
+            }
+            fclose (f2);
+            fprintf (stderr, "type return");
+            char c;
+            fscanf (stdin, "%c", &c);
+# endif
+            newStat = pmAstromGridAngle (rot, ref, config);
+            newStat->angle  = angle;
+            newStat->scale  = scale;
+            newStat->center = center;
+
+            if (isfinite(newStat->minMetric) && (newStat->minMetric > 0.0) && (newStat->minMetric < minStat->minMetric)) {
+                *minStat = *newStat;
+                psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric) *",
+                          minStat->offset.x, minStat->offset.y, PS_DEG_RAD*minStat->angle, minStat->scale, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+            } else {
+                psLogMsg ("psModule.astrom", 4, "grid test - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+                          newStat->offset.x, newStat->offset.y, PS_DEG_RAD*newStat->angle, newStat->scale, newStat->nMatch, newStat->nSigma, newStat->minVar, log10(newStat->minMetric));
+
+            }
+            psFree (newStat);
+            psFree (rot);
+        }
+    }
+    psLogMsg ("psModule.astrom.grid.match", 4, "grid best - offset: %7.2f,%7.2f @ %6.1f deg x %7.3f (%4d pts, %5.1f sig, %5.1f var, %6.3f log metric)",
+              minStat->offset.x, minStat->offset.y, PS_DEG_RAD*minStat->angle, minStat->scale, minStat->nMatch, minStat->nSigma, minStat->minVar, log10(minStat->minMetric));
+
+    // I need to decide if a solution is likely to be a good solution or just a mis-match
+    // one posibility: how significant is the peak relative to the 4th or 5th most significant pixel?
+
+    if (minStat->nSigma < minSigma) {
+        psError(PS_ERR_UNKNOWN, true, "Failed to find a valid match (%f sigma for best)", minStat->nSigma);
+        psFree (minStat);
+        return NULL;
+    }
+    return (minStat);
+}
+
+/******************************************************************************
+pmAstromGridTweak(*raw, *ref, *recipe, stats): improve match for two star lists.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridTweak(
+    psArray *raw,
+    psArray *ref,
+    psMetadata *recipe,
+    pmAstromStats *stats)
+{
+    bool status;
+    pmAstromObj *ob1, *ob2;  // short-cut pointers to the objects
+    double dX, dY;   // offset between a possible matched pair
+    psArray *rot;
+    int nBin, xBin, yBin;
+
+    rot = pmAstromRotateObj (raw, stats->center, stats->angle, stats->scale);
+
+    // sampling scale of the grid
+    double tweakScale  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SCALE");
+    double tweakRange  = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.RANGE");
+    double tweakSmooth = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.SMOOTH");
+    double tweakNsigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.TWEAK.NSIGMA");
+
+    nBin = 2*tweakRange / tweakScale;
+    psVector *xHist = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVector *yHist = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVectorInit (xHist, 0);
+    psVectorInit (yHist, 0);
+
+    // accumulate grids for focal plane (L,M) matches
+    for (int i = 0; i < rot->n; i++) {
+        ob1 = (pmAstromObj *)rot->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x - stats->offset.x;
+            dY = ob1->FP->y - ob2->FP->y - stats->offset.y;
+
+            xBin = (dX + tweakRange) / tweakScale;
+            yBin = (dY + tweakRange) / tweakScale;
+
+            if (xBin < 0)
+                continue;
+            if (yBin < 0)
+                continue;
+            if (xBin >= nBin)
+                continue;
+            if (yBin >= nBin)
+                continue;
+
+            xHist->data.F32[xBin] += 1.0;
+            yHist->data.F32[yBin] += 1.0;
+        }
+    }
+
+    // smooth histgram vector with gaussian of 1sigma = radius
+    psVector *xHistNew = psVectorSmooth(NULL, xHist, tweakSmooth, tweakNsigma);
+    psVector *yHistNew = psVectorSmooth(NULL, yHist, tweakSmooth, tweakNsigma);
+
+    // select peak in x and in y
+    xBin = yBin = 0;
+    double xMax = 0;
+    double yMax = 0;
+    for (int i = 0; i < nBin; i++) {
+        if (xHistNew->data.F32[i] > xMax) {
+            xBin = i;
+            xMax = xHistNew->data.F32[i];
+        }
+        if (yHistNew->data.F32[i] > yMax) {
+            yBin = i;
+            yMax = yHistNew->data.F32[i];
+        }
+    }
+    double xPeak = xBin*tweakScale - tweakRange;
+    double yPeak = yBin*tweakScale - tweakRange;
+    psLogMsg (__func__, 3, "tweak peak by %f,%f\n", xPeak, yPeak);
+
+    // adjust offset by peak center
+    pmAstromStats *tweak = pmAstromStatsAlloc();
+    *tweak = *stats;
+    tweak->offset.x += xPeak;
+    tweak->offset.y += yPeak;
+
+    pmAstromVisualPlotTweak (xHistNew, yHistNew, xBin, yBin);
+
+    psFree (rot);
+    psFree (xHist);
+    psFree (yHist);
+    psFree (xHistNew);
+    psFree (yHistNew);
+
+    return tweak;
+}
+
+/******************************************************************************
+pmAstromGridApply(*map, stat): apply the measured FPA offset and rotation
+(stat) to the fpa astrom structures.
+ ******************************************************************************/
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat)
+{
+    PS_ASSERT_PTR_NON_NULL(map, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->x, NULL);
+    PS_ASSERT_POLY_NON_NULL(map->y, NULL);
+
+    double cs = stat->scale * cos (stat->angle);
+    double sn = stat->scale * sin (stat->angle);
+
+    double dx = (map->x->coeff[0][0] - stat->center.x);
+    double dy = (map->y->coeff[0][0] - stat->center.y);
+
+    // new offset
+    map->x->coeff[0][0] =  cs*dx + sn*dy - stat->offset.x + stat->center.x;
+    map->y->coeff[0][0] = -sn*dx + cs*dy - stat->offset.y + stat->center.y;
+
+    // original rotation matrix
+    double pc1_1 = map->x->coeff[1][0];
+    double pc1_2 = map->x->coeff[0][1];
+    double pc2_1 = map->y->coeff[1][0];
+    double pc2_2 = map->y->coeff[0][1];
+
+    // new rotation matrix
+    map->x->coeff[1][0] = +cs*pc1_1 + sn*pc2_1;
+    map->x->coeff[0][1] = +cs*pc1_2 + sn*pc2_2;
+    map->y->coeff[1][0] = -sn*pc1_1 + cs*pc2_1;
+    map->y->coeff[0][1] = -sn*pc1_2 + cs*pc2_2;
+
+    return (map);
+}
+
+/* Illustration of the grid bins
+   dX        bin
+   -35:-25 -> 0     bin = dX / Scale + Offset
+   -25:-15 -> 1     Scale = 10
+   -15:-05 -> 2     Offset = 3.5
+   -05:+05 -> 3     nPix = 3 (maxOffset = 35 = (nPix + 0.5)*dXix
+   +05:+15 -> 4     dPix = 10
+   +15:+25 -> 5
+   +25:+35 -> 6
+
+   maxOffsetRequest = 30
+   nPix = (int) (maxOffset / dPix + 0.5);
+   maxOffset = (nPix + 0.5)*Scale;
+*/
+
Index: /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.c	(revision 21432)
@@ -0,0 +1,1358 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAfile.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryVisual.h"
+#include "pmFPAExtent.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+# include "pmKapaPlots.h"
+#include "pmVisual.h"
+
+//variables to determine when things are plotted
+static bool isVisual             = false;
+static bool plotGridMatch        = true;
+static bool plotTweak            = true;
+static bool plotRawStars         = true;
+static bool plotRefStars         = false;
+static bool plotLumFunc          = true;
+static bool plotRemoveClumps     = true;
+static bool plotOneChipFit       = true;
+static bool plotFixChips         = true;
+static bool plotAstromGuessCheck = true;
+static bool plotMosaicMatches    = true;
+static bool plotCommonScale      = true;
+static bool plotMosaicOneChip    = true;
+
+// variables to store plotting window indices
+static int kapa = -1;
+static int kapa2 = -1;
+
+//helper prototypes
+bool residPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe,
+                char *title);
+
+
+/* Initialization Routines  */
+
+bool pmAstromSetVisual (bool mode) {
+    isVisual = mode;
+    return true;
+}
+
+
+bool pmAstromVisualClose()
+{
+    if(kapa != -1)
+        KiiClose(kapa);
+    if(kapa2 != -1)
+        KiiClose(kapa2);
+    return true;
+}
+
+
+/* Plotting Routines */
+bool pmAstromVisualPlotRawStars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    // make sure we want to plot this
+    if (!plotRawStars || !isVisual) return true;
+
+    //set up plot region
+    if (!pmVisualInitWindow (&kapa, "psastro:plots")){
+        isVisual = false;
+        return false;
+    }
+    Graphdata graphdata;
+    KapaSection section;
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.4;
+    section.dy = 0.4;
+
+    //initialize and populate plotting vectors
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    //Chip coordinates
+    int n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel(kapa, "Chip", KAPA_LABEL_XP);
+    KapaSendLabel(kapa, "X", KAPA_LABEL_XM);
+    KapaSendLabel(kapa, "Y", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    //Focal Plane Coordinates
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->FP->x;
+        yVec->data.F32[n] = raw->FP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "Focal Plane", KAPA_LABEL_XP);
+    KapaSendLabel (kapa, "L", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "M", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Tangent Plane Coordinates
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->TP->x;
+        yVec->data.F32[n] = raw->TP->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "Tangential Plane", KAPA_LABEL_XP);
+    KapaSendLabel (kapa, "P", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Q", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    //sky coordinates
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = DEG_RAD*raw->sky->r;
+        yVec->data.F32[n] = DEG_RAD*raw->sky->d;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "Sky", KAPA_LABEL_XP);
+    KapaSendLabel(kapa, "RA", KAPA_LABEL_XM);
+    KapaSendLabel(kapa, "Dec", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa, &graphdata);
+
+    // plot label
+    section.x = 0.0;
+    section.y = 0.0;
+    section.dx = .95;
+    section.dy = .95;
+    section.name = NULL;
+    psStringAppend (&section.name, "a5");
+    KapaSetSection (kapa, &section);
+    KapaSendLabel (kapa, "Raw Star Selection - Initial Astrometry", KAPA_LABEL_XP);
+    psFree (section.name);
+
+    // pause and wait for user input:
+    pmVisualAskUser(&plotRawStars, &isVisual);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe)
+{
+    //make sure we want to plot this
+    if (!isVisual || !plotRefStars) return true;
+
+    //set up plotting variables
+    if (!pmVisualInitWindow (&kapa, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    Graphdata graphdata;
+    KapaInitGraph (&graphdata);
+    KapaClearSections (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    //initialize and populate plot vectors
+    bool status = false;
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        if (!isfinite(ref->Mag)) continue;
+        if (ref->Mag > rMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+
+        xVec->data.F32[n] = DEG_RAD*ref->sky->r;
+        yVec->data.F32[n] = DEG_RAD*ref->sky->d;
+        zVec->data.F32[n] = ref->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "RA", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Dec", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Reference Stars", KAPA_LABEL_XP);
+    pmVisualTriplePlot(kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // flip x (East increase to left)
+    SWAP (graphdata.xmin, graphdata.xmax);
+    KapaSetLimits (kapa, &graphdata);
+
+    // pause and wait for user input:
+    pmVisualAskUser(&plotRefStars, &isVisual);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag,   // Log(n) for each magnitude bin
+                                          psVector *Mag,     // magnitude bins
+                                          pmLumFunc *lumFunc,// Fit to the reference star luminosity function
+                                          pmLumFunc *rawFunc // Fit to the raw star luminoisty function
+                                          )
+{
+
+    // make sure we want to plot this
+    if ( !isVisual || !plotLumFunc ) return true;
+
+    //set up plotting variables
+    if ( !pmVisualInitWindow (&kapa, "psastro:plots")){
+        isVisual = false;
+        return false;
+    }
+
+    Graphdata graphdata;
+    KapaSection section1 = {"s1", .1, .1, .85, .35};
+    KapaSection section2 =  {"s2", .1, .5, .85, .35};
+    KapaSection section;
+    if(rawFunc == NULL)
+        section = section1;
+    else
+        section = section2;
+    if (rawFunc == NULL)
+        KapaClearPlots(kapa);
+    KapaInitGraph(&graphdata);
+
+    //Determine Plot Limits
+    pmVisualScaleGraphdata(&graphdata, Mag, lnMag, false);
+
+    //Make a line for the fit
+    float x[2] = {graphdata.xmin, graphdata.xmax};
+    float y[2] = {lumFunc->offset + x[0] * lumFunc->slope,
+                 lumFunc->offset + x[1] * lumFunc->slope};
+
+    //Plot Data
+    KapaSetSection(kapa, &section);
+    KapaSetLimits(kapa, &graphdata);
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "Magnitude", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Log(N)", KAPA_LABEL_YM);
+    if (rawFunc == NULL)
+        KapaSendLabel (kapa, "Raw Star Luminosity Function", KAPA_LABEL_XP);
+    else
+        KapaSendLabel (kapa,
+                       "Reference Star Luminosity Function, Shifted Raw Fit, and Cutoff",
+                       KAPA_LABEL_XP);
+    graphdata.color=KapaColorByName("black");
+    graphdata.style = 1;
+    KapaPrepPlot (kapa, lnMag->n, &graphdata);
+    KapaPlotVector(kapa, lnMag->n,   Mag->data.F32, "x");
+    KapaPlotVector(kapa, lnMag->n, lnMag->data.F32, "y");
+
+    //Overplot fit
+    graphdata.style=0;
+    KapaPrepPlot(kapa,2,&graphdata);
+    KapaPlotVector(kapa, 2, x, "x");
+    KapaPlotVector(kapa, 2, y, "y");
+
+    //If rawFunc was supplied, overplot the raw star fit + cutoff
+    if( rawFunc != NULL) {
+        double mRef = 0.5*(lumFunc->mMin + lumFunc->mMax);
+        double logRho = mRef * lumFunc->slope + lumFunc->offset;
+        double mRaw = (logRho - rawFunc->offset) / rawFunc->slope;
+        double deltaM = mRef - mRaw;
+        double mRefMax = rawFunc->mMax + deltaM;
+
+        float xraw[2] = {rawFunc->mMin + deltaM, rawFunc->mMax + deltaM};
+        float yraw[2] = {rawFunc->offset + (rawFunc->slope) * rawFunc->mMin,
+                        rawFunc->offset + (rawFunc->slope) * rawFunc->mMax};
+        float x[2] = {mRefMax, mRefMax};
+        float y[2] = {graphdata.ymin, graphdata.ymax};
+        graphdata.color= KapaColorByName("red");
+        KapaPrepPlot(kapa, 2, &graphdata);
+        KapaPlotVector(kapa, 2, x, "x");
+        KapaPlotVector(kapa, 2, y, "y");
+        KapaPrepPlot (kapa, 2, &graphdata);
+        KapaPlotVector (kapa, 2, xraw, "x");
+        KapaPlotVector (kapa, 2, yraw, "y");
+
+        // pause and wait for user input:
+        pmVisualAskUser (&plotLumFunc, &isVisual);
+    }
+    return true;
+} // end of pmAstromVisualPlotLuminosityFunction
+
+
+bool pmAstromVisualPlotRemoveClumps (psArray *input, // Array containing the field stars
+                                    psImage *count, // A 2D histogram of the field star distribution
+                                    int scale,      // The pixel size of the histogram
+                                    float limit     // The minimum numuber of stars in a bin flagged as a clump
+                                    )
+{
+
+    //make sure we want to plot this
+    if (!isVisual || !plotRemoveClumps) return true;
+
+    //set up plot variables
+    if ( !pmVisualInitWindow (&kapa, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    KapaSection section;
+    Graphdata graphdata;
+    section.x = 0;
+    section.dx = .9;
+    section.y = 0;
+    section.dy = .9;
+    section.name = NULL;
+    psStringAppend( &section.name, "a0");
+    KapaInitGraph(&graphdata);
+    KapaSetSection(kapa, &section);
+    psFree(section.name);
+
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    graphdata.color = KapaColorByName ("black");
+    KapaClearPlots(kapa);
+
+    //set up plot vectors
+    float Xmin = +FLT_MAX;
+    float Xmax = -FLT_MAX;
+    float Ymin = +FLT_MAX;
+    float Ymax = -FLT_MAX;
+    psVector *xVec = psVectorAlloc (input->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (input->n, PS_TYPE_F32);
+
+    //determine boundaries for histogram bin calculation
+    int n = 0;
+    for (int i=0; i< input->n; i++) {
+        pmAstromObj *obj = (pmAstromObj *)input->data[i];
+        if (!isfinite(obj->FP->x)) continue;
+        if (!isfinite(obj->FP->y)) continue;
+        xVec->data.F32[n] = obj->FP->x;
+        yVec->data.F32[n] = obj->FP->y;
+        Xmin = PS_MIN (Xmin, xVec->data.F32[n]);
+        Xmax = PS_MAX (Xmax, xVec->data.F32[n]);
+        Ymin = PS_MIN (Ymin, yVec->data.F32[n]);
+        Ymax = PS_MAX (Ymax, yVec->data.F32[n]);
+        n++;
+    }
+    xVec->n = yVec->n = n;
+
+    //plot stars
+    graphdata.xmax = Xmax;
+    graphdata.xmin = Xmin;
+    graphdata.ymax = Ymax;
+    graphdata.ymin = Ymin;
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont (kapa, "helvetica", 14);
+
+    KapaBox (kapa, &graphdata);
+
+    KapaSendLabel (kapa, "L (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "M (pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Regions Flagged as Clumps (Red Boxes)",
+                   KAPA_LABEL_XP);
+
+    KapaPrepPlot (kapa, xVec->n, &graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = 1;
+
+    //overplot clumpy regions excluded from analysis
+    for(int i = 0; i < count->numCols; i++) {
+        for (int j = 0; j < count->numRows; j++) {
+            if(count->data.U32[j][i] <= limit) continue; //not a clump
+            float Xbot = (i - 5) * scale + Xmin;
+            float Ybot = (j - 5) * scale + Ymin;
+            if(Xbot < graphdata.xmin || Xbot > graphdata.xmax ||
+               Ybot < graphdata.ymin || Ybot > graphdata.ymax) continue;
+            float x[5] = {Xbot, Xbot + scale, Xbot + scale, Xbot, Xbot};
+            float y[5] = {Ybot, Ybot, Ybot + scale, Ybot + scale, Ybot};
+            KapaPrepPlot (kapa, 5, &graphdata);
+            KapaPlotVector (kapa, 5, x, "x");
+            KapaPlotVector (kapa, 5, y, "y");
+        }
+    }
+
+    //ask for user input and finish
+    pmVisualAskUser (&plotRemoveClumps, &isVisual);
+    psFree (xVec);
+    psFree (yVec);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, // stars detected in the image
+                                  psArray *refstars, // reference stars over the same region
+                                  psArray *match,    // contains which rawstars match to which refstars
+                                  psMetadata *recipe // data reduction recipe
+                                  )
+{
+
+    //make sure we want to plot this
+    if (!isVisual || !plotOneChipFit)
+        return true;
+
+    if(!pmVisualInitWindow(&kapa, "psastro:plots") || !pmVisualInitWindow(&kapa2, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    //plot the residuals
+    if (!residPlot(rawstars, refstars, match, recipe, "Single Chip Fit Residuals (Chip Coordinates)")) {
+        plotOneChipFit = false;
+        return false;
+    }
+
+    //ask for user input and finish
+    pmVisualAskUser(&plotOneChipFit, &isVisual);
+    return true;
+}
+
+
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, // focal plane array file
+                                psVector *xOld, // old X location of chip cornerss
+                                psVector *yOld // old Y location of chip corners
+                                )
+{
+    //make sure we want to plot this
+    if(!isVisual || !plotFixChips) return true;
+
+    if(!pmVisualInitWindow(&kapa, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    KapaSection section = {"s1", .05, .05, .9, .9};
+    Graphdata graphdata;
+    KapaInitGraph( &graphdata);
+    KapaClearPlots (kapa);
+    graphdata.ptype = 2;
+    graphdata.style = 2;
+
+    psVector *xNew = psVectorAllocEmpty (xOld->n, PS_TYPE_F32);
+    psVector *yNew = psVectorAllocEmpty (yOld->n, PS_TYPE_F32);
+
+    // copy of the code in psastroFixChips that generated xOld, yOld, but for xNew, yNew
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    pmChip *obsChip = NULL;
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+        psRegion *region = pmChipPixels(obsChip);
+        psPlane ptCP, ptFP;
+
+        ptCP.x = region->x0; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x0; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x1; ptCP.y = region->y1;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        ptCP.x = region->x1; ptCP.y = region->y0;
+        psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+        psVectorAppend (xNew, ptFP.x);
+        psVectorAppend (yNew, ptFP.y);
+
+        psFree (region);
+    }
+
+    //set up graph
+    pmVisualScaleGraphdata(&graphdata, xOld, yOld, true);
+    pmVisualInitGraph(kapa, &section, &graphdata);
+    KapaSendLabel (kapa, "L (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "M (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Chip corners before (black) and after (red) FixChips", KAPA_LABEL_XP);
+    KapaPrepPlot (kapa, xOld->n, &graphdata);
+    KapaPlotVector (kapa, xOld->n, xOld->data.F32, "x");
+    KapaPlotVector (kapa, xOld->n, yOld->data.F32, "y");
+
+    graphdata.ptype = 1;
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot (kapa, xNew->n, &graphdata);
+    KapaPlotVector (kapa, xNew->n, xNew->data.F32, "x");
+    KapaPlotVector (kapa, xNew->n, yNew->data.F32, "y");
+
+    pmVisualAskUser(&plotFixChips, &isVisual);
+    psFree(xNew);
+    psFree(yNew);
+    psFree(view);
+    return true;
+}
+
+
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, // P coordinates of chip corners before fitting
+                                        psVector *cornerQo, // Q coordinates of chip corners before fitting
+                                        psVector *cornerPn, // P coordinates of chip corners after fitting
+                                        psVector *cornerQn, // Q coordinates of chip corners after fitting
+                                        psVector *cornerPd, // P coordinate residuals of fit from old to new coordinates
+                                        psVector *cornerQd  // Q coordinate residuals of fit from old to new coordinates
+                                        )
+{
+
+    //make sure we want to plot this
+    if (!isVisual || !plotAstromGuessCheck) return true;
+
+    //set up graph window
+    if ( !pmVisualInitWindow (&kapa, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    Graphdata graphdata;
+    KapaSection section;
+    KapaInitGraph(&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.4;
+    section.dy = 0.4;
+
+    //Old Corners
+    section.x = 0.30;
+    section.y = 0.50;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree(section.name);
+
+    pmVisualScaleGraphdata (&graphdata, cornerPo, cornerPo, true);
+    KapaSetLimits (kapa, &graphdata);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "P (Pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Q (Pixels)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa,
+                   "Fiducial Points in the Tangent Plane. Black: Initial Astrometry. Red: Final Astrometry",
+                   KAPA_LABEL_XP);
+    KapaPrepPlot (kapa, cornerPo->n, &graphdata);
+    KapaPlotVector (kapa, cornerPo->n, cornerPo->data.F32, "x");
+    KapaPlotVector (kapa, cornerQo->n, cornerQo->data.F32, "y");
+
+    // New Corners
+    graphdata.color = KapaColorByName("red");
+    graphdata.ptype = 7;
+    graphdata.size = 1.5;
+    KapaPrepPlot (kapa, cornerPn->n, &graphdata);
+    KapaPlotVector (kapa, cornerPn->n, cornerPn->data.F32, "x");
+    KapaPlotVector (kapa, cornerQn->n, cornerQn->data.F32, "y");
+
+    // Residuals
+    psVector *xResid = psVectorAlloc(cornerPn->n, PS_DATA_F32);
+    psVector *yResid = psVectorAlloc(cornerQn->n, PS_DATA_F32);
+    for(int i=0; i < cornerPn->n; i++) {
+        xResid->data.F32[i] = (cornerPd->data.F32[i]);
+        yResid->data.F32[i] = (cornerQd->data.F32[i]);
+    }
+
+    graphdata.color = KapaColorByName("black");
+    graphdata.size=0.5;
+    section.x = 0.3;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree(section.name);
+
+    pmVisualScaleGraphdata (&graphdata, xResid, yResid, true);
+    KapaSetLimits (kapa, &graphdata);
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "dP", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "dQ", KAPA_LABEL_YM);
+    KapaSendLabel (kapa,
+                   "Residual of the Fit from the Initial Astrometry to the Final Astrometry",
+                   KAPA_LABEL_XP);
+    KapaPrepPlot (kapa, cornerPd->n, &graphdata);
+    KapaPlotVector (kapa, cornerPd->n, xResid->data.F32, "x");
+    KapaPlotVector (kapa, cornerQd->n, yResid->data.F32, "y");
+
+    psFree(xResid);
+    psFree(yResid);
+    pmVisualAskUser (&plotAstromGuessCheck, &isVisual);
+    return true;
+}
+
+
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa,         // the fpa
+                                   psVector *oldScale  // the old pixel scale of each chip in the fpa
+                                   )
+{
+    //make sure we want to plot this
+    if (!isVisual || !plotCommonScale) return true;
+
+    if (!pmVisualInitWindow(&kapa, "psastro:plots")){
+        isVisual = false;
+        return false;
+    }
+
+    KapaSection section = {"s1", .05, .05, .9, .9};
+    Graphdata graphdata;
+    psPlane ptCH, ptFP;
+    ptCH.x = 0;
+    ptCH.y = 0;
+    psVector *xVec = psVectorAlloc (oldScale->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (oldScale->n, PS_TYPE_F32);
+
+    int nobj = 0;
+
+    //project each chip corner to the Focal Plane
+    for(int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->toFPA) { continue; }
+
+        psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+        xVec->data.F32[nobj] = ptFP.x;
+        yVec->data.F32[nobj] = ptFP.y;
+        nobj++;
+        if (nobj == oldScale->n) break;
+    }
+
+    //set up plot window
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+    KapaSetSection (kapa, &section);
+    KapaSetFont (kapa, "helvetica", 14);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, oldScale, false);
+    KapaSendLabel (kapa, "L (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "M (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Old Pixel Scale of FPA Chips (Not to Scale)", KAPA_LABEL_XP);
+
+    pmVisualAskUser (&plotCommonScale, &isVisual);
+
+    psFree(xVec);
+    psFree(yVec);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars,
+                                     psArray *match, psMetadata *recipe)
+{
+
+    //make sure we want to plot this
+    if (!isVisual || !plotMosaicOneChip) return true;
+
+    if(!pmVisualInitWindow(&kapa, "psastro:plots") || !pmVisualInitWindow(&kapa2, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    //plot the residuals
+    if (!residPlot(rawstars, refstars, match, recipe, "Single Chip Fit Residuals - Mosaic Mode")) {
+        isVisual = false;
+        return false;
+    }
+
+    //ask for user input and finish
+    pmVisualAskUser(&plotMosaicOneChip, &isVisual);
+
+    return true;
+}
+
+
+bool pmAstromVisualPlotMosaicMatches( psArray *rawstars, psArray *refstars,
+                                    psArray *match, int iteration,
+                                    psMetadata *recipe)
+{
+    //make sure we want to plot this
+    if (!isVisual || !plotMosaicMatches) return true;
+
+    char title[60];
+    sprintf(title, "Matches found during psastroMosaicSetMatch iteration %d", iteration);
+
+
+    if(!pmVisualInitWindow(&kapa, "psastro:plots") || !pmVisualInitWindow(&kapa2, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    if (!residPlot(rawstars, refstars, match, recipe, title)){
+        isVisual = false;
+        return false;
+    }
+
+    //ask for user input
+    pmVisualAskUser (&plotMosaicMatches, &isVisual);
+    return true;
+}
+
+
+bool pmAstromVisualPlotGridMatch (const psArray *raw,
+                                  const psArray *ref,
+                                  psImage *gridNP,
+                                  double offsetX,
+                                  double offsetY,
+                                  double maxOffpix,
+                                  double Scale,
+                                  double Offset)
+{
+    //make sure we want to plot this
+    if (!isVisual || !plotGridMatch) return true;
+    if (!pmVisualInitWindow(&kapa, "psastro:plots")){
+        isVisual = false;
+        return false;
+    }
+
+    KapaSection section = {"s1", 0.05, 0.05, .75, .75};
+    KapaSection sectionY = {"s2", 0.8, 0.05, .15, .75};
+    KapaSection sectionX = {"s3", .05, .8, .75, .15};
+
+    Graphdata graphdata;
+    int nplot = raw->n * ref->n;                      // number of points to plot
+    psVector *dXplot = psVectorAlloc (nplot, PS_TYPE_F32); // x data points
+    psVector *dYplot = psVectorAlloc (nplot, PS_TYPE_F32); // y data points
+
+    pmAstromObj *ob1; pmAstromObj *ob2;               // shortcuts to the data in raw and ref
+    psU32 **NP = gridNP->data.U32;                    // shortcut to the gridNP data
+    float vertHistSlice[gridNP->numRows];             // vertical histogram slice through peak
+    float horizHistSlice[gridNP->numCols];            // horizontal histogram slice through peak
+    float horizontalIndices[gridNP->numCols];         // the horizontal offset corresponding to each bin of horizHistSlice
+    float verticalIndices[gridNP->numRows];           // the vertical offset corresponding to each bin of vertHistSlice
+    int maxHorizontalSlice = 0;                       // peak value of horizHistSlice
+    int maxVerticalSlice = 0;                         // peak value of vertHistSlice
+    int peakXbin = (int) (offsetX / Scale + Offset);  // X bin index of peak
+    int peakYbin = (int) (offsetY / Scale + Offset);  // Y bin index of peak
+
+    // psVector *vertHistSlice = psVectorAlloc (gridNP->numRows, PS_TYPE_F32);
+
+    // set up plot information
+    KapaClearPlots(kapa);
+    KapaInitGraph(&graphdata);
+    KapaSetSection(kapa, &section);
+
+    graphdata.xmin = -1.0 * maxOffpix;
+    graphdata.xmax =  1.0 * maxOffpix;
+    graphdata.ymin = -1.0 * maxOffpix;
+    graphdata.ymax =  1.0 * maxOffpix;
+    KapaSetLimits(kapa, &graphdata);
+
+    KapaSetFont(kapa, "helvetica", 14);
+    KapaBox(kapa, &graphdata);
+    KapaSendLabel (kapa, "X offset (FP)", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Y offset (FP)", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "pmAstromGridAngle residuals. Box: Correlation Peak.",
+                   KAPA_LABEL_XP);
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 0.4;
+    graphdata.color = KapaColorByName ("black");
+
+    // calculate the plot points
+    float dX, dY;
+    for (int i = 0; i < raw->n; i++) {
+        ob1 = (pmAstromObj *)raw->data[i];
+        for (int j = 0; j < ref->n; j++) {
+            ob2 = (pmAstromObj *)ref->data[j];
+            dX = ob1->FP->x - ob2->FP->x;
+            dY = ob1->FP->y - ob2->FP->y;
+            dXplot->data.F32[(i * ref->n) + j] = dX;
+            dYplot->data.F32[(i * ref->n) + j] = dY;
+        }
+    }
+
+    // calculate the points for the profiles
+    for (int i = 0; i < gridNP->numRows; i++) {
+        vertHistSlice[i] = NP[i][peakXbin];
+        verticalIndices[i] = (i - Offset) * Scale;
+        if (vertHistSlice[i] > maxVerticalSlice) {
+            maxVerticalSlice = vertHistSlice[i];
+        }
+    }
+    for (int i = 0; i < gridNP->numCols; i++) {
+        horizHistSlice[i] = NP[peakYbin][i];
+        horizontalIndices[i] = (i - Offset) * Scale;
+        if (horizHistSlice[i] > maxHorizontalSlice) {
+            maxHorizontalSlice = horizHistSlice[i];
+        }
+    }
+
+    //Plot the offsets
+    KapaPrepPlot(kapa, nplot, &graphdata);
+    KapaPlotVector (kapa, nplot, dXplot->data.F32, "x");
+    KapaPlotVector (kapa, nplot, dYplot->data.F32, "y");
+
+    //Overplot bounding box, peak of distribution
+    float xbound[5] = { -maxOffpix, maxOffpix, maxOffpix, -maxOffpix, -maxOffpix};
+    float ybound[5] = { -maxOffpix, -maxOffpix, maxOffpix, maxOffpix, -maxOffpix};
+    float xbin[5] = {offsetX - 0.5 * Scale, offsetX + 0.5 * Scale,
+                     offsetX + 0.5 * Scale, offsetX - 0.5 * Scale,
+                     offsetX - 0.5 * Scale};
+    float ybin[5] = {offsetY - 0.5 * Scale, offsetY - 0.5 * Scale,
+                     offsetY + 0.5 * Scale, offsetY + 0.5 * Scale,
+                     offsetY - 0.5 * Scale};
+    graphdata.color = KapaColorByName("red");
+    graphdata.style = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot(kapa, 5, &graphdata);
+    KapaPlotVector (kapa, 5, xbound, "x");
+    KapaPlotVector (kapa, 5, ybound, "y");
+    KapaPrepPlot(kapa, 5, &graphdata);
+    KapaPlotVector (kapa, 5, xbin, "x");
+    KapaPlotVector (kapa, 5, ybin, "y");
+
+    //plot X profile
+    KapaSetSection(kapa, &sectionX);
+    graphdata.color = KapaColorByName("black");
+    graphdata.ptype = 1;
+    graphdata.style = 1;
+    graphdata.ymin = 0;
+    graphdata.ymax = maxHorizontalSlice + 0.5;
+    KapaSetLimits(kapa, &graphdata);
+
+    KapaBox(kapa, &graphdata);
+    KapaPrepPlot(kapa, gridNP->numCols, &graphdata);
+    KapaPlotVector (kapa, gridNP->numCols, horizontalIndices, "x");
+    KapaPlotVector (kapa, gridNP->numCols, horizHistSlice, "y");
+    float xslice[2] = {offsetX - Scale / 2., offsetX - Scale / 2.};
+    float yslice[2] = {-5, 100};
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot(kapa, 2, &graphdata);
+    KapaPlotVector (kapa, 2, xslice, "x");
+    KapaPlotVector (kapa, 2, yslice, "y");
+
+    //plot Y profile
+    KapaSetSection(kapa, &sectionY);
+    graphdata.color = KapaColorByName("black");
+    graphdata.ptype = 1;
+    graphdata.style = 1;
+    graphdata.ymin = -maxOffpix;
+    graphdata.ymax = maxOffpix;
+    graphdata.xmin = -1.0 ;
+    graphdata.xmax = maxVerticalSlice + 0.5;
+    KapaSetLimits(kapa, &graphdata);
+
+    KapaBox(kapa, &graphdata);
+    KapaPrepPlot(kapa, gridNP->numRows, &graphdata);
+    KapaPlotVector (kapa, gridNP->numRows, vertHistSlice, "x");
+    KapaPlotVector (kapa, gridNP->numRows, verticalIndices, "y");
+    yslice[0] = yslice[1] = offsetY - Scale / 2.;
+    xslice[0] = -5; xslice[1] = 100;
+    graphdata.color = KapaColorByName("red");
+    KapaPrepPlot(kapa, 2, &graphdata);
+    KapaPlotVector (kapa, 2, xslice, "x");
+    KapaPlotVector (kapa, 2, yslice, "y");
+
+    pmVisualAskUser(&plotGridMatch, &isVisual);
+    psFree(dXplot);
+    psFree(dYplot);
+    return true;
+} // end of pmAstromVisualPlotGridMatch
+
+
+bool pmAstromVisualPlotTweak (psVector *xHist, // Smoothed Horizontal cut through the histogram
+                              psVector *yHist, // Smoothed Vertical cut throug the histogram
+                              int xBin,        // X Bin index of the histogram peak
+                              int yBin         // Y bin index of the histogram peak
+    )
+{
+    //make sure we want to plot this
+    if (!isVisual || !plotTweak) return true;
+    if (!pmVisualInitWindow(&kapa, "psastro:plots")) {
+        isVisual = false;
+        return false;
+    }
+
+    Graphdata graphdata;
+    KapaSection section1 = {"s1", 0.05, 0.05, 0.90, 0.4};
+    KapaSection section2 = {"s2", 0.05, 0.5, 0.90, 0.4};
+
+    psVector *xIndices = psVectorAlloc (xHist->n, PS_TYPE_F32);
+    psVector *yIndices = psVectorAlloc (yHist->n, PS_TYPE_F32);
+
+    //populate the Indices vectors
+    for(int i = 0; i < xHist->n; i++) {
+        xIndices->data.F32[i] = i;
+    }
+    for(int i = 0; i < yHist->n; i++) {
+        yIndices->data.F32[i] = i;
+    }
+
+    // set up plot information
+    KapaClearPlots(kapa);
+    KapaInitGraph(&graphdata);
+
+    // plot the X histogram
+    pmVisualScaleGraphdata(&graphdata, xIndices, xHist, false);
+    KapaSetSection(kapa, &section1);
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont(kapa, "helvetica", 14);
+    KapaBox(kapa, &graphdata);
+    KapaSendLabel (kapa, "X offset Bin", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Number of Sources", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Horizontal Profile",
+                   KAPA_LABEL_XP);
+    graphdata.style = 1;
+    graphdata.ptype = 0;
+    graphdata.size = 0.4;
+    graphdata.color = KapaColorByName ("black");
+
+    KapaPrepPlot (kapa, xHist->n, &graphdata);
+    KapaPlotVector (kapa, xHist->n, xIndices->data.F32, "x");
+    KapaPlotVector (kapa, xHist->n, xHist->data.F32, "y");
+
+    //overplot the peak
+    float x[2] = {xBin, xBin};
+    float y[2] = {-500, 500};
+    graphdata.color = KapaColorByName ("red");
+    KapaPrepPlot (kapa, 2, &graphdata);
+    KapaPlotVector (kapa, 2, x, "x");
+    KapaPlotVector (kapa, 2, y, "y");
+
+    //plot the Y histogram
+    pmVisualScaleGraphdata(&graphdata, yIndices, yHist, false);
+    KapaSetSection(kapa, &section2);
+    KapaSetLimits (kapa, &graphdata);
+    KapaSetFont(kapa, "helvetica", 14);
+    graphdata.color = KapaColorByName ("black");
+    KapaBox(kapa, &graphdata);
+    KapaSendLabel (kapa, "Y offset Bin", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "Number of Sources", KAPA_LABEL_YM);
+    KapaSendLabel (kapa, "Vertical Profile",
+                   KAPA_LABEL_XP);
+    graphdata.style = 1;
+    graphdata.ptype = 0;
+    graphdata.size = 0.4;
+
+    KapaPrepPlot (kapa, yHist->n, &graphdata);
+    KapaPlotVector (kapa, yHist->n, yIndices->data.F32, "x");
+    KapaPlotVector (kapa, yHist->n, yHist->data.F32, "y");
+
+    //overplot the peak
+    x[0] = x[1] = yBin;
+    graphdata.color = KapaColorByName ("red");
+    KapaPrepPlot (kapa, 2, &graphdata);
+    KapaPlotVector (kapa, 2, x, "x");
+    KapaPlotVector (kapa, 2, y, "y");
+
+    //plot title
+    graphdata.color = KapaColorByName("black");
+    KapaSection section3 = {"s3", 0, 0, 1, 1};
+    KapaSetSection( kapa, &section3);
+    KapaSendLabel (kapa, "Tweaking the Astrometry Grid Solution. Smoothed profiles + peak location",
+                   KAPA_LABEL_XP);
+
+    pmVisualAskUser(&plotTweak, &isVisual);
+
+    psFree(xIndices);
+    psFree(yIndices);
+    return true;
+} //end of pmAstromPlotTweak
+
+
+bool residPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe,
+                        char *title) {
+
+
+    //initialize graph information
+    Graphdata graphdata;
+    KapaSection section;
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.4;
+    section.dy = 0.4;
+
+    //initialize and populate the plotting vectors
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    psVector *xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    psVector *zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    // X vs dX
+    section.x = 0.0;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a0");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    int n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "dX", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // X vs dY
+    section.x = 0.5;
+    section.y = 0.5;
+    section.name = NULL;
+    psStringAppend (&section.name, "a1");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "dY", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dX
+    section.x = 0.0;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a2");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->x - ref->chip->x;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "Y", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "dX", KAPA_LABEL_YM);
+    pmVisualTriplePlot (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // Y vs dY
+    section.x = 0.5;
+    section.y = 0.0;
+    section.name = NULL;
+    psStringAppend (&section.name, "a3");
+    KapaSetSection (kapa, &section);
+    psFree (section.name);
+
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->y;
+        yVec->data.F32[n] = raw->chip->y - ref->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa, "Y", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "dY", KAPA_LABEL_YM);
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    section.x = 0.0;
+    section.y = 0.0;
+    section.dx = 0.95;
+    section.dy = 0.95;
+    section.name = NULL;
+    psStringAppend (&section.name, "a5");
+    KapaSetSection (kapa, &section);
+    KapaSendLabel (kapa, title, KAPA_LABEL_XP);
+    psFree (section.name);
+
+    //second window
+
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa2);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.style = 2;
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (rawstars->n, PS_TYPE_F32);
+
+    // X vs Y by mag (raw)
+    n = 0;
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+        if (!isfinite(raw->Mag)) continue;
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = raw->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+
+    KapaSendLabel (kapa2, "X", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "Y", KAPA_LABEL_YM);
+    KapaSendLabel (kapa2,
+                   "Chip Coordinates. Black = Raw Stars. Red = Ref Stars. Blue = Matched Stars"
+                   , KAPA_LABEL_XP);
+    pmVisualTriplePlot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // X vs Y by mag (ref)
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (refstars->n, PS_TYPE_F32);
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.style = 2;
+
+    n = 0;
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        if (!isfinite(ref->Mag)) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = ref->chip->x;
+        yVec->data.F32[n] = ref->chip->y;
+        zVec->data.F32[n] = ref->Mag;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmVisualTripleOverplot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    //rescale the graph to include all points
+    float xmin = graphdata.xmin;
+    float ymin = graphdata.ymin;
+    float xmax = graphdata.xmax;
+    float ymax = graphdata.ymax;
+    pmVisualScaleGraphdata(&graphdata, xVec, yVec, true);
+    graphdata.xmin = PS_MIN(xmin, graphdata.xmin);
+    graphdata.ymin = PS_MIN(ymin, graphdata.ymin);
+    graphdata.xmax = PS_MAX(xmax, graphdata.xmax);
+    graphdata.ymax = PS_MAX(ymax, graphdata.ymax);
+    KapaSetLimits (kapa2, &graphdata);
+
+    //overplot matched stars in blue
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+
+    xVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    yVec = psVectorAlloc (match->n, PS_TYPE_F32);
+    zVec = psVectorAlloc (match->n, PS_TYPE_F32);
+
+    graphdata.color = KapaColorByName ("blue");
+    n = 0;
+    for (int i = 0; i < match->n; i++) {
+        pmAstromMatch *pair = match->data[i];
+        pmAstromObj *raw = rawstars->data[pair->raw];
+        pmAstromObj *ref = refstars->data[pair->ref];
+        if (raw->Mag < iMagMin) continue;
+        if (raw->Mag > iMagMax) continue;
+        if (ref->Mag < rMagMin) continue;
+        if (ref->Mag > rMagMax) continue;
+
+        xVec->data.F32[n] = raw->chip->x;
+        yVec->data.F32[n] = raw->chip->y;
+        zVec->data.F32[n] = iMagMin;
+        n++;
+    }
+    xVec->n = yVec->n = zVec->n = n;
+    pmVisualTripleOverplot (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+
+
+
+# else
+
+bool pmAstromSetVisual(bool mode) { return true; }
+bool pmAstromVisualClose() { return true; }
+bool pmAstromVisualPlotGridMatch (const psArray *raw, const psArray *ref, psImage *gridNP, double offsetX, double offsetY, double maxOffpix, double Scale, double Offset) { return true; }
+bool pmAstromVisualPlotTweak (psVector *xHist, psVector *yHist, int xBin, int yBin) {return true;}
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag, psVector *Mag, pmLumFunc *lumFunc, pmLumFunc *rawFunc) {return true;}
+bool pmAstromVisualPlotRawStars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotRemoveClumps (psArray *input, psImage *count, int scale, float limit) {return true;}
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, psVector *xOld, psVector *yOld) {return true;}
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, psVector *cornerQo, psVector *cornerPn, psVector *cornerQn, psVector *cornerPd, psVector *cornerQd) {return true;}
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {return true;}
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa, psVector *oldScale) {return true;}
+bool pmAstromVisualPlotMosaicMatches (psArray *rawstars, psArray *refstars, psArray *match, int iteration, psMetadata *recipe) {return true;}
+
+# endif
Index: /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.h
===================================================================
--- /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.h	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryVisual.h	(revision 21432)
@@ -0,0 +1,169 @@
+/*
+ * @file pmAstrometryVisual.h
+ * @author Chris Beaumont, IfA
+ * @brief A set of functions to display visual diagnostics from psastro
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROM_VISUAL_H
+#define PM_ASTROM_VISUAL_H
+
+
+/** A fit to the logN / logS curve for a set of stars
+ * logN = offset + slope * logS
+ */
+typedef struct {
+    double mMin;                        ///< minimum magnitude bin with data
+    double mMax;                        ///< maximum magnitude bin with data
+    double offset;                      ///< fitted line offset
+    double slope;                       ///< fitted line slope
+    double mPeak;                       ///< mag of peak bin
+    int nPeak;                          ///< # of stars in peak bin
+    int sPeak;                          ///< sum of stars to peak bin
+} pmLumFunc;
+
+
+/** Enable or disable visual plotting for psastro routines
+ * @param mode true/false to enable/disable plotting
+ * @return true for success */
+bool pmAstromSetVisual(bool mode);
+
+
+/** Close plotting windows at the end of a run
+ * @return true for success */
+bool pmAstromVisualClose();
+
+
+/**
+ * Plot the offset between every pair of reference and raw source locations. The peak of this
+ * distribution nominally gives the offset, scale difference, and rotation of the two catalogs.
+ * Overplots the location of this peak as determined by pmAstromGridAngle, as well as some profiles
+ * along horizontal and vertical cuts through this peak.
+ */
+bool pmAstromVisualPlotGridMatch (const psArray *raw, ///< raw stars
+                                  const psArray *ref, ///< reference stars
+                                  psImage *gridNP,    ///< a 2D histogram of raw-ref star distances
+                                  double offsetX,     ///< The X location (FP coordinates) of the peak of gridNP
+                                  double offsetY,     ///< the Y location (FP coordinates) of the peak of gridNP
+                                  double maxOffpix,   ///< The half-width of gridNP in FP coordinates
+                                  double Scale,       ///< The pixel size of gridNP in histogram-bin-coordinates
+                                  double Offset       ///< The (x,y) location (histogram-bin coordinates) of the FP point (0,0) in gridNP
+                                  );
+
+
+/**
+ * Plot the refinements made within pmAstromGridTweak.
+ * After pmAstromGridMatch finds the best rotaion/scale/offset between raw and reference stars
+ * within a coarse grid of rotations/scales, pmAstromGridTweak computes a higher precision
+ * estimate of the offset. It computes the 2 point correlation function between raw and ref
+ * stars along horizontal and vertical cuts through the first-guess offset. It finds the peak
+ * of these two profiles and adjusts the offset accordingly. This procedure plots the profiles.
+ */
+bool pmAstromVisualPlotTweak (psVector *xHist, ///< Smoothed Horizontal cut through the histogram
+                              psVector *yHist, ///< Smoothed Vertical cut throug the histogram
+                              int xBin,        ///< X Bin index of the histogram peak
+                              int yBin         ///< Y bin index of the histogram peak
+                              );
+
+
+/**
+ * Plot the two luminosity functions created within psastroRefStarSubset
+ * The luminosity functions are used to select a subset of reference stars,
+ * so we plot the cutoff that defines this subset
+ */
+bool pmAstromVisualPlotLuminosityFunction (psVector *lnMag,   ///< Log(n) for each magnitude bin
+                                          psVector *Mag,     ///< magnitude bins
+                                          pmLumFunc *lumFunc,///< Fit to the reference star luminosity function
+                                          pmLumFunc *rawFunc ///< Fit to the raw star luminoisty function
+                                           );
+
+
+/**
+ * Plot raw stars as determined from first pass astrometry fit
+ * Called within psastroAstromGeuss
+ */
+bool pmAstromVisualPlotRawStars (psArray *rawstars, ///< Stars detected in the fpa
+                                 pmFPA *fpa,  ///< structure describing the focal plane array
+                                 pmChip *chip,  ///< structure describing the chip
+                                 psMetadata *recipe ///< the recipe used in psastro
+                                 );
+
+
+/**
+ * plot the location of references stars over the entire fpa
+ * invoked during psastroChooseRefStars
+ */
+bool pmAstromVisualPlotRefStars (psArray *refstars, psMetadata *recipe);
+
+
+/**
+ * Plot the stars in a region, and indicate which stars are part of 'clumps'
+ * These stars are flagged during astrometric fitting, since dense regions are
+ * harder to cross-match than sparse ones. Called during psastroRemoveClumps.
+ */
+bool pmAstromVisualPlotRemoveClumps (psArray *input, ///< Array containing the field stars
+                                    psImage *count, ///< A 2D histogram of the field star distribution
+                                    int scale,      ///< The pixel size of the histogram
+                                    float limit     ///< The minimum numuber of stars in a bin flagged as a clump
+                                     );
+
+/**
+ * Plots the chip corners in the FP before and after chips with inconsistent solutions have been fixed.
+ * Invoked during psastroFixChips
+ */
+bool pmAstromVisualPlotFixChips (pmFPAfile *input, ///< focal plane array file
+                                 psVector *xOld, ///< old X location of chip cornerss
+                                 psVector *yOld ///< old Y location of chip corners
+                                 );
+
+
+/**
+ * Assess the goodness of fit for a signle chip by
+ * plotting the fit residuals
+ * invoked during psastroOneChipFit
+ */
+bool pmAstromVisualPlotOneChipFit (psArray *rawstars, ///< stars detected in the image
+                                  psArray *refstars, ///< reference stars over the same region
+                                  psArray *match,    ///< contains which rawstars match to which refstars
+                                  psMetadata *recipe ///< data reduction recipe
+                                   );
+
+/**
+ *  Plots the fpa chip corners projected on to the tangential plane before and after
+ *  the astrometry solution has been applied. In psastroAstromGuessCheck, the old corners
+ *  are then fit to the new corners to get a sense at how far off the initial WCS info was
+ *  in offset, rotation, and scale. This procedure also plots the residuals of the fit from
+ *  old to new coordinates
+ */
+bool pmAstromVisualPlotAstromGuessCheck (psVector *cornerPo, ///< P coordinates of chip corners before fitting
+                                        psVector *cornerQo, ///< Q coordinates of chip corners before fitting
+                                        psVector *cornerPn, ///< P coordinates of chip corners after fitting
+                                        psVector *cornerQn, ///< Q coordinates of chip corners after fitting
+                                        psVector *cornerPd, ///< P coordinate residuals of fit from old to new coordinates
+                                        psVector *cornerQd  ///< Q coordinate residuals of fit from old to new coordinates
+                                         );
+
+
+/**
+ *   plot the residuals between raw stars and ref stars after
+ *   fitting in psastroMosaicOneChip
+ */
+bool pmAstromVisualPlotMosaicOneChip (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) ;
+
+
+/**
+ * Plots the pixel scales of the fpa before they are
+ * equalized in psastroMosaicCommonScale
+ */
+bool pmAstromVisualPlotCommonScale (pmFPA *fpa,         ///< the fpa
+                                   psVector *oldScale  ///< the old pixel scale of each chip in the fpa
+                                    );
+
+
+/** pmAstromVisualPlotMosaicMatches
+ * Plot the matches between raw and reference stars during pmAstromVisualMosaicSetMatch
+ */
+bool pmAstromVisualPlotMosaicMatches (psArray *rawstars, psArray *refstars, psArray *match, int iteration, psMetadata *recipe);
+
+
+#endif
Index: /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryWCS.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryWCS.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/astrom/pmAstrometryWCS.c	(revision 21432)
@@ -0,0 +1,968 @@
+/** @file  pmAstrometryWCS.c
+ *
+ *  @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.35 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2009-02-10 20:55:47 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <strings.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryWCS.h"
+#include "pmAstrometryUtils.h"
+#include "pmAstrometryRegions.h"
+
+// the following functions support coordinate transformations direcly related to the FITS WCS
+// keywords.  The FITS WCS allows for only a single level of transformation, thus it is not
+// appropriate for mosaic astrometry consisting of telescope distortion plus chip terms.
+// Below, we support the Elixir convention of using two connected FITS headers to define two
+// levels of coordinate transformation.  In the pmFPA structure, the projection, distortion,
+// and FPA-to-Chip transformations are carried independently.  NOTE: The FITS WCS keywords do
+// not represent a simple polynomial.  Instead, they have no constant term, and the coordinates
+// are corrected to a reference pixel before the polynomial transformation is applied.
+
+// interpret header WCS (only handles traditional WCS for the moment)
+// pixelScale is microns per pixel
+bool pmAstromReadWCS (pmFPA *fpa, pmChip *chip, const psMetadata *header, double pixelScale)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCStoFPA (fpa, chip, wcs, pixelScale);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is convergence for inversion of non-linear terms in pixels
+bool pmAstromWriteWCS (psMetadata *header, const pmFPA *fpa, const pmChip *chip, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromFPA(fpa, chip, tol);
+    if (!wcs) return false;
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+// interpret chip header WCS as bilevel chip components
+bool pmAstromReadBilevelChip (pmChip *chip, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        return false;
+    }
+
+    bool status = pmAstromWCSBileveltoChip (chip, wcs);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert toFPA / toSky components to traditional WCS
+// we require the header to have NAXIS1,NAXIS2, the field of the FPA
+// the center of the TPA/Sky projection is 0.5*(NAXIS1,NAXIS2)
+bool pmAstromReadBilevelMosaic (pmFPA *fpa, const psMetadata *header)
+{
+    pmAstromWCS *wcs = pmAstromWCSfromHeader (header);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from header");
+        return false;
+    }
+
+    bool status1 = false;
+    bool status2 = false;
+    int Nx = psMetadataLookupS32 (&status1, header, "NAXIS1");
+    int Ny = psMetadataLookupS32 (&status2, header, "NAXIS2");
+
+    if (!status1 || !status2) {
+        Nx = psMetadataLookupS32 (&status1, header, "ZNAXIS1");
+        Ny = psMetadataLookupS32 (&status2, header, "ZNAXIS2");
+    }
+
+    if (!status1 || !status2) {
+        psFree (wcs);
+        psError(PS_ERR_UNKNOWN, false, "missing required FPA size in header");
+        return false;
+    }
+
+    psRegion region = psRegionSet (-0.5*Nx, +0.5*Nx, -0.5*Ny, +0.5*Ny);
+    bool status = pmAstromWCSBileveltoFPA (fpa, wcs, region);
+
+    psFree (wcs);
+    return status;
+}
+
+// convert chip->toFPA components to bilevel WCS
+bool pmAstromWriteBilevelChip (psMetadata *header, const pmChip *chip, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelChipFromFPA (chip, tol);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from fpa");
+        return false;
+    }
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (wcs);
+    return true;
+}
+
+
+// convert fpa->toTPA, fpa->toSky components to bilevel WCS
+bool pmAstromWriteBilevelMosaic (psMetadata *header, const pmFPA *fpa, double tol)
+{
+    pmAstromWCS *wcs = pmAstromWCSBilevelMosaicFromFPA (fpa, tol);
+    if (!wcs) {
+        psError(PS_ERR_UNKNOWN, false, "failure to determine WCS terms from fpa");
+        return false;
+    }
+
+    // we need to specify the dimensions of the FPA
+    // if we have chips defined, we can do
+    psRegion *region = pmAstromFPAExtent (fpa);
+    int Nx = region->x1 - region->x0;
+    int Ny = region->y1 - region->y0;
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NAXIS1", PS_META_REPLACE, "Mosaic Dimensions", Nx);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NAXIS2", PS_META_REPLACE, "Mosaic Dimensions", Ny);
+
+    pmAstromWCStoHeader (header, wcs);
+
+    psFree (region);
+    psFree (wcs);
+    return true;
+}
+
+// convert coordinates from chip to sky using a pmAstromWCS structure
+bool pmAstromWCStoSky (psSphere *sky, pmAstromWCS *wcs, psPlane *chip)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    Chip->x = chip->x - wcs->crpix1;
+    Chip->y = chip->y - wcs->crpix2;
+
+    psPlaneTransformApply (FP, wcs->trans, Chip);
+    psDeproject (sky, FP, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// convert coordinates from sky to chip using a pmAstromWCS structure
+bool pmAstromWCStoChip (psPlane *chip, pmAstromWCS *wcs, psSphere *sky)
+{
+
+    if (chip == NULL)
+        return false;
+    if (sky == NULL)
+        return false;
+    if (wcs == NULL)
+        return false;
+
+    psError(PS_ERR_UNKNOWN, true, "not yet implemented: needs to invert the transformation");
+    return false;
+
+    psPlane *Chip = psPlaneAlloc();
+    psPlane *FP = psPlaneAlloc();
+
+    psProject (FP, sky, wcs->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    // XXX I actually need the inverse of wcs->transform at this point
+    psPlaneTransformApply (Chip, wcs->trans, FP);
+
+    chip->x = Chip->x + wcs->crpix1;
+    chip->y = Chip->y + wcs->crpix2;
+
+    psFree (Chip);
+    psFree (FP);
+    return true;
+}
+
+// interpret header WCS keywords (valid for bilevel and traditional WCS)
+pmAstromWCS *pmAstromWCSfromHeader (const psMetadata *header)
+{
+    psProjectionType type;
+    bool status, pcKeys, cdKeys, isPoly;
+    char name[16]; // used to store FITS keyword below (always < 8, so 16 should be safe!)
+
+    // interpret header data, convert to crval(i), etc
+    char *ctype = psMetadataLookupPtr (&status, header, "CTYPE2");
+    if (!status) {
+        psLogMsg ("psastro", 5, "warning: no WCS metadata in header\n");
+        return NULL;
+    }
+
+    // determine projection type
+    // XXX there are two indications for higher-order terms: the type (DIS,WRP,PLY,ZPL) and
+    // the value of NPLYTERM.
+    type = psProjectTypeFromString (ctype);
+    if (type == PS_PROJ_NTYPE) {
+        psLogMsg ("psastro", 2, "warning: unknown projection type %s\n", ctype);
+        return NULL;
+    }
+
+    // what type of WCS keywords are available?
+    // XXX add check for CROTA2
+    int fitOrder = psMetadataLookupS32 (&isPoly, header, "NPLYTERM");
+    psMetadataLookupF64 (&pcKeys, header, "PC001001");
+    psMetadataLookupF64 (&cdKeys, header, "CD1_1");
+
+    if (cdKeys && pcKeys) {
+        // XXX make this an option
+        psLogMsg ("psastro", 5, "warning: both CDi_j and PC00i00j defined in headers, using PC00i00j terms\n");
+    }
+    if (!cdKeys && !pcKeys) {
+        psError(PS_ERR_UNKNOWN, true, "missing both CDi_j and PC00i00j WCS terms");
+        // XXX we could default here to RA, DEC, ROTANGLE
+        return NULL;
+    }
+    if (isPoly) {
+        if (!pcKeys) {
+            psError(PS_ERR_UNKNOWN, true, "polynomial terms defined, but missing PC00i00j WCS terms");
+            return NULL;
+        }
+        if (fitOrder == 0)
+            fitOrder = 1;
+        if ((fitOrder > 3) || (fitOrder < 1)) {
+            psError(PS_ERR_UNKNOWN, true, "NPLYTERM value undefined: %d", fitOrder);
+            return NULL;
+        }
+    } else {
+        fitOrder = 1;
+    }
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc (fitOrder, fitOrder);
+
+    // construct a transformation from X,Y in pixels to L,M in pixels
+    // NOTE that the WCS keywords convert X,Y to degrees first (using cdelt1,2)
+    // and then define a transformation from degrees to degrees
+
+    wcs->crval1 = psMetadataLookupF64 (&status, header, "CRVAL1");
+    wcs->crval2 = psMetadataLookupF64 (&status, header, "CRVAL2");
+    wcs->crpix1 = psMetadataLookupF64 (&status, header, "CRPIX1");
+    wcs->crpix2 = psMetadataLookupF64 (&status, header, "CRPIX2");
+    wcs->toSky = psProjectionAlloc (wcs->crval1*PM_RAD_DEG, wcs->crval2*PM_RAD_DEG, PM_RAD_DEG, PM_RAD_DEG, type);
+
+    // These aren't needed but having them empty is disconcerting
+    strncpy(wcs->ctype2, ctype, PM_ASTROM_WCS_TYPE_SIZE-1);
+    ctype = psMetadataLookupStr (&status, header, "CTYPE1");
+    strncpy(wcs->ctype1, ctype, PM_ASTROM_WCS_TYPE_SIZE-1);
+    wcs->ctype1[PM_ASTROM_WCS_TYPE_SIZE-1] = 0;
+    wcs->ctype2[PM_ASTROM_WCS_TYPE_SIZE-1] = 0;
+
+    // XXX I think this is wrong for linear proj
+
+    // test the CDELTi varient
+    if (pcKeys) {
+        wcs->cdelt1 = psMetadataLookupF64 (&status, header, "CDELT1");
+        wcs->cdelt2 = psMetadataLookupF64 (&status, header, "CDELT2");
+
+        // test the CROTAi varient:
+        // XXX double check lambda..
+        double rotate = psMetadataLookupF64 (&status, header, "CROTA2");
+        if (status) {
+            wcs->trans->x->coeff[1][0] = +wcs->cdelt1 * cos(rotate*PM_RAD_DEG); // == PC1_1
+            wcs->trans->x->coeff[0][1] = -wcs->cdelt2 * sin(rotate*PM_RAD_DEG); // == PC1_2
+            wcs->trans->y->coeff[1][0] = +wcs->cdelt1 * sin(rotate*PM_RAD_DEG); // == PC2_1
+            wcs->trans->y->coeff[0][1] = +wcs->cdelt2 * cos(rotate*PM_RAD_DEG); // == PC2_2
+            return wcs;
+        }
+
+        // FITS WCS PCi,j has units of unity
+        // wcs->trans has units of degrees/pixel
+        wcs->trans->x->coeff[1][0] = wcs->cdelt1 * psMetadataLookupF64 (&status, header, "PC001001"); // == PC1_1
+        wcs->trans->x->coeff[0][1] = wcs->cdelt2 * psMetadataLookupF64 (&status, header, "PC001002"); // == PC1_2
+        wcs->trans->y->coeff[1][0] = wcs->cdelt1 * psMetadataLookupF64 (&status, header, "PC002001"); // == PC2_1
+        wcs->trans->y->coeff[0][1] = wcs->cdelt2 * psMetadataLookupF64 (&status, header, "PC002002"); // == PC2_2
+
+        if (isPoly) {
+            // Elixir-style polynomial terms
+            // XXX currently, Elixir/DVO cannot accept mixed orders
+            for (int i = 0; i <= fitOrder; i++) {
+                for (int j = 0; j <= fitOrder; j++) {
+                    if (i + j < 2)
+                        continue;
+                    if (i + j > fitOrder) {
+                        wcs->trans->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+                        wcs->trans->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+                        continue;
+                    }
+                    sprintf (name, "PCA1X%1dY%1d", i, j);
+                    wcs->trans->x->coeff[i][j] = pow(wcs->cdelt1, i) * pow(wcs->cdelt2, j) * psMetadataLookupF64 (&status, header, name);
+                    sprintf (name, "PCA2X%1dY%1d", i, j);
+                    wcs->trans->y->coeff[i][j] = pow(wcs->cdelt1, i) * pow(wcs->cdelt2, j) * psMetadataLookupF64 (&status, header, name);
+                }
+            }
+        }
+        return wcs;
+    }
+
+    // test the CDi_j varient
+    if (cdKeys) {
+        wcs->trans->x->coeff[1][0] = psMetadataLookupF64 (&status, header, "CD1_1"); // == PC1_1
+        wcs->trans->x->coeff[0][1] = psMetadataLookupF64 (&status, header, "CD1_2"); // == PC1_2
+        wcs->trans->y->coeff[1][0] = psMetadataLookupF64 (&status, header, "CD2_1"); // == PC2_1
+        wcs->trans->y->coeff[0][1] = psMetadataLookupF64 (&status, header, "CD2_2"); // == PC2_2
+        wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+        wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+        return wcs;
+    }
+    psLogMsg ("psastro", 2, "warning: missing rotation matrix?\n");
+    psFree (wcs);
+    return NULL;
+}
+
+// convert wcs transformations into header WCS keywords (only handles traditional WCS for the moment)
+// wcs->trans defines the transformation from pixels to degrees.
+// wcs->cdelt1,2 carries the original pixels scale.
+// XXX force PC00i00j to be normalized, or use cdelt1,2 to set the scale?
+// here I've chosen to force the rotation matrix to be normalized
+bool pmAstromWCStoHeader (psMetadata *header, const pmAstromWCS *wcs)
+{
+    char name[16]; // used to store FITS keyword below (always < 8, so 16 should be safe!)
+    char *type;
+
+    if (!wcs) return false;
+
+    type = psProjectTypeToString (wcs->toSky->type, "RA--");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE1", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    type = psProjectTypeToString (wcs->toSky->type, "DEC-");
+    psMetadataAddStr (header, PS_LIST_TAIL, "CTYPE2", PS_META_REPLACE, "", type);
+    psFree (type);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL1", PS_META_REPLACE, "", wcs->toSky->R*PS_DEG_RAD);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRVAL2", PS_META_REPLACE, "", wcs->toSky->D*PS_DEG_RAD);
+
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX1", PS_META_REPLACE, "", wcs->crpix1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CRPIX2", PS_META_REPLACE, "", wcs->crpix2);
+
+    // XXX make it optional to write out CDi_j terms, or other versions
+    // apply CDELT1,2 (degrees / pixel) to yield PCi,j terms of order unity
+    double cdelt1 = wcs->cdelt1;
+    double cdelt2 = wcs->cdelt2;
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT1", PS_META_REPLACE, "", cdelt1);
+    psMetadataAddF64 (header, PS_LIST_TAIL, "CDELT2", PS_META_REPLACE, "", cdelt2);
+
+    // test the PC00i00j varient:
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001001", PS_META_REPLACE, "", wcs->trans->x->coeff[1][0] / cdelt1); // == PC1_1
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC001002", PS_META_REPLACE, "", wcs->trans->x->coeff[0][1] / cdelt2); // == PC1_2
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002001", PS_META_REPLACE, "", wcs->trans->y->coeff[1][0] / cdelt1); // == PC2_1
+    psMetadataAddF64 (header, PS_LIST_TAIL, "PC002002", PS_META_REPLACE, "", wcs->trans->y->coeff[0][1] / cdelt2); // == PC2_2
+
+    // Elixir-style polynomial terms
+    // XXX currently, Elixir/DVO cannot accept mixed orders
+    // XXX need to respect the masks
+    // XXX is wcs->cdelt1,2 always consistent?
+    int fitOrder = wcs->trans->x->nX;
+    if (fitOrder > 1) {
+        for (int i = 0; i <= fitOrder; i++) {
+            for (int j = 0; j <= fitOrder; j++) {
+                if (i + j < 2)
+                    continue;
+                if (i + j > fitOrder)
+                    continue;
+                sprintf (name, "PCA1X%1dY%1d", i, j);
+                psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->trans->x->coeff[i][j] / pow(cdelt1, i) / pow(cdelt2, j));
+                sprintf (name, "PCA2X%1dY%1d", i, j);
+                psMetadataAddF64 (header, PS_LIST_TAIL, name, PS_META_REPLACE, "", wcs->trans->y->coeff[i][j] / pow(cdelt1, i) / pow(cdelt2, j));
+            }
+        }
+        psMetadataAddS32 (header, PS_LIST_TAIL, "NPLYTERM", PS_META_REPLACE, "", fitOrder);
+    }
+
+    // remove any existing 'CDi_j style' wcs keywords
+    if (psMetadataLookup(header, "CD1_1")) {
+        psMetadataRemoveKey(header, "CD1_1");
+        psMetadataRemoveKey(header, "CD1_2");
+        psMetadataRemoveKey(header, "CD2_1");
+        psMetadataRemoveKey(header, "CD2_2");
+    }
+
+    return true;
+}
+
+// interpret header WCS (only handles traditional WCS for the moment)
+// pixelScale is the pixel size in microns/pixel
+bool pmAstromWCStoFPA (pmFPA *fpa, pmChip *chip, const pmAstromWCS *wcs, double pixelScale)
+{
+    psPlaneTransform *toFPA;
+
+    // create transformation with 0,0 reference pixel and units of degrees/pixel
+    toFPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // modify scale of toFPA to have units of microns/pixel
+    // cdelt1,2 has units of degree/pixel
+    for (int i = 0; i <= toFPA->x->nX; i++) {
+        for (int j = 0; j <= toFPA->x->nX; j++) {
+            toFPA->x->coeff[i][j] *= pixelScale/wcs->cdelt1;
+            toFPA->y->coeff[i][j] *= pixelScale/wcs->cdelt2;
+        }
+    }
+
+    // pdelt1,2 has units of degree/micron
+    double pdelt1 = wcs->cdelt1 / pixelScale;
+    double pdelt2 = wcs->cdelt2 / pixelScale;
+
+    // projection from TPA (linear microns) to SKY (radians)
+    psProjection *toSky = psProjectionAlloc (wcs->toSky->R, wcs->toSky->D, PM_RAD_DEG*pdelt1, PM_RAD_DEG*pdelt2, wcs->toSky->type);
+
+    if (fpa->toSky == NULL) {
+        fpa->toTPA = psPlaneTransformIdentity (1);
+        fpa->fromTPA = psPlaneTransformIdentity (1);
+        fpa->toSky = toSky;
+    } else {
+
+        // this section allows the loaded chip to be included in an fpa structure in which
+        // other chips have already been loaded (ie, the fpa->toTPA, fpa->toSky components have
+        // already been defined).  we have to adjust to match the existing transformation.
+
+        if (fpa->toTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+        if (fpa->fromTPA == NULL)
+            psAbort("projection defined, tangent-plane not defined");
+
+        // convert from pixels on this chip to pixels on reference chip
+        // rX has units of refpixels / pixel
+        double rX = toSky->Xs / fpa->toSky->Xs;
+        double rY = toSky->Ys / fpa->toSky->Ys;
+
+        for (int i = 0; i <= toFPA->x->nX; i++) {
+            for (int j = 0; j <= toFPA->x->nY; j++) {
+                toFPA->x->coeff[i][j] *= rX;
+                toFPA->y->coeff[i][j] *= rY;
+            }
+        }
+
+        // apply the exiting fromTPA transformation to make the new toFPA consistent with the toTPA layter
+        // XXX this only works if toTPA is at most a linear transformation
+        psPlaneTransform *toFPAnew = psPlaneTransformAlloc(toFPA->x->nX, toFPA->x->nY);
+        for (int i = 0; i <= toFPA->x->nX; i++) {
+          for (int j = 0; j <= toFPA->x->nY; j++) {
+            double f1 = toFPA->x->coeffMask[i][j] ? 0.0 : fpa->fromTPA->x->coeff[1][0]*toFPA->x->coeff[i][j];
+            double f2 = toFPA->y->coeffMask[i][j] ? 0.0 : fpa->fromTPA->x->coeff[0][1]*toFPA->y->coeff[i][j];
+            toFPAnew->x->coeff[i][j] = f1 + f2;
+
+            double g1 = toFPA->x->coeffMask[i][j] ? 0.0 : fpa->fromTPA->y->coeff[1][0]*toFPA->x->coeff[i][j];
+            double g2 = toFPA->y->coeffMask[i][j] ? 0.0 : fpa->fromTPA->y->coeff[0][1]*toFPA->y->coeff[i][j];
+            toFPAnew->y->coeff[i][j] = g1 + g2;
+          }
+        }
+        toFPAnew->x->coeff[0][0] += fpa->fromTPA->x->coeff[0][0];
+        toFPAnew->y->coeff[0][0] += fpa->fromTPA->y->coeff[0][0];
+
+        psFree (toFPA);
+        toFPA = toFPAnew;
+
+        // adjust reference pixel for new toSky reference coordinate
+        // find the FPA coordinate of 0,0 for this chip.
+        psPlane *fpOld = psPlaneAlloc();
+        psPlane *fpNew = psPlaneAlloc();
+        psPlane *tp = psPlaneAlloc();
+        psSphere *sky = psSphereAlloc();
+
+        sky->r = toSky->R;
+        sky->d = toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpOld, fpa->fromTPA, tp);
+
+        sky->r = fpa->toSky->R;
+        sky->d = fpa->toSky->D;
+        psProject (tp, sky, fpa->toSky); // find the focal-plane coord of this RA,DEC coord using the ref chip projection
+        psPlaneTransformApply (fpNew, fpa->fromTPA, tp);
+
+        toFPA->x->coeff[0][0] -= fpNew->x - fpOld->x;
+        toFPA->y->coeff[0][0] -= fpNew->y - fpOld->y;
+
+        psFree (sky);
+        psFree (tp);
+        psFree (fpOld);
+        psFree (fpNew);
+
+        psFree (toSky);
+    }
+
+    // free an existing toFPA structure
+    psFree (chip->toFPA);
+    chip->toFPA = toFPA;
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    // this can take a very long time...
+    while (fpa->toSky->R < 0)
+        fpa->toSky->R += 2.0*M_PI;
+    while (fpa->toSky->R > 2.0*M_PI)
+        fpa->toSky->R -= 2.0*M_PI;
+
+    psTrace ("psastro", 5, "toFPA: %f %f  (%f,%f),(%f,%f)\n",
+             chip->toFPA->x->coeff[0][0], chip->toFPA->y->coeff[0][0],
+             chip->toFPA->x->coeff[1][0], chip->toFPA->x->coeff[0][1],
+             chip->toFPA->y->coeff[1][0], chip->toFPA->y->coeff[0][1]);
+
+    psTrace ("psastro", 5, "frFPA: %f %f  (%f,%f),(%f,%f)\n",
+             chip->fromFPA->x->coeff[0][0], chip->fromFPA->y->coeff[0][0],
+             chip->fromFPA->x->coeff[1][0], chip->fromFPA->x->coeff[0][1],
+             chip->fromFPA->y->coeff[1][0], chip->fromFPA->y->coeff[0][1]);
+
+    return true;
+}
+
+// convert a pmAstromWCS structure representing a bilevel chip into corresponding chip elements
+bool pmAstromWCSBileveltoChip (pmChip *chip, const pmAstromWCS *wcs)
+{
+    /* we convert wcs->trans to toFPA, which is different from wcs->trans in 3 important ways:
+     * 1) the output is in pixel (not degrees): divide by cdelt1,2 raised to an appropriate power
+     * 2) X,Y are applied directly, without an applied Xo,Yo offset
+     * 3) there is an allowed Lo,Mo term ([0][0] coefficients)
+     */
+
+    // create transformation with 0,0 reference pixel and units of microns/pixel
+    psFree (chip->toFPA);
+    chip->toFPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // determine the inverse transformation: we need the chip pixels covered by this transform
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    return true;
+}
+
+// convert a pmAstromWCS structure representing a bilevel mosaic into corresponding fpa elements
+bool pmAstromWCSBileveltoFPA (pmFPA *fpa, const pmAstromWCS *wcs, psRegion region)
+{
+    // projection from TPA (microns) to SKY (radians)
+    // cdelt1,2 has units of degrees/micron
+    fpa->toSky = psProjectionAlloc (wcs->toSky->R, wcs->toSky->D, wcs->cdelt1*PM_RAD_DEG, wcs->cdelt2*PM_RAD_DEG, wcs->toSky->type);
+
+    // create transformation with 0,0 reference pixel
+    fpa->toTPA = psPlaneTransformSetCenter (NULL, wcs->trans, -wcs->crpix1, -wcs->crpix2);
+
+    // convert fpa->toTPA to units of unity (microns/micron)
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            fpa->toTPA->x->coeff[i][j] /= wcs->cdelt1;
+            fpa->toTPA->y->coeff[i][j] /= wcs->cdelt2;
+        }
+    }
+
+    // the transformation used the region to define the inversion grid
+    // the region defines the FPA pixels covered by the tranformation
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, region, 50);
+    return true;
+}
+
+// convert toFPA / toSky components to pmAstromWCS
+// tolerance is allowed error in center solution in pixels
+pmAstromWCS *pmAstromWCSfromFPA (const pmFPA *fpa, const pmChip *chip, double tol)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+    PS_ASSERT_PTR_NON_NULL(chip->toFPA, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, NULL);
+
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    // technically, we can have a plate scale here (fpa->toTPA:dx,dy != 1)
+    // XXX not really: toTPA needs to have unity scale for distortion fitting function
+    // if (!psPlaneTransformIsDiagonal (fpa->toTPA))
+    // psAbort("invalid TPA transformation");
+
+    // create a temporary transform which combines toTPA and toFPA.  allow toTPA to have 0th
+    // and 1st order terms
+
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->x->nY
+    // XXX require fpa->toTPA->y->nX == fpa->toTPA->y->nY
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->y->nX
+    // XXX require fpa->toTPA->x->coeffMask[1][1]
+    // XXX require fpa->toTPA->y->coeffMask[1][1]
+    // XXX require fpa->toTPA->nX == 1
+
+    psPlaneTransform *toTPA = psPlaneTransformAlloc(chip->toFPA->x->nX, chip->toFPA->x->nY);
+
+    for (int i = 0; i <= toTPA->x->nX; i++) {
+        for (int j = 0; j <= toTPA->x->nY; j++) {
+            double f1 = chip->toFPA->x->coeffMask[i][j] ? 0.0 : fpa->toTPA->x->coeff[1][0]*chip->toFPA->x->coeff[i][j];
+            double f2 = chip->toFPA->y->coeffMask[i][j] ? 0.0 : fpa->toTPA->x->coeff[0][1]*chip->toFPA->y->coeff[i][j];
+            toTPA->x->coeff[i][j] = f1 + f2;
+
+            double g1 = chip->toFPA->x->coeffMask[i][j] ? 0.0 : fpa->toTPA->y->coeff[1][0]*chip->toFPA->x->coeff[i][j];
+            double g2 = chip->toFPA->y->coeffMask[i][j] ? 0.0 : fpa->toTPA->y->coeff[0][1]*chip->toFPA->y->coeff[i][j];
+            toTPA->y->coeff[i][j] = g1 + g2;
+        }
+    }
+    toTPA->x->coeff[0][0] += fpa->toTPA->x->coeff[0][0];
+    toTPA->y->coeff[0][0] += fpa->toTPA->y->coeff[0][0];
+
+    pmAstromWCS *wcs = pmAstromWCSAlloc(toTPA->x->nX, toTPA->x->nY);
+
+    // convert projection from FPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, fpa->toSky->type);
+    wcs->crval1 = fpa->toSky->R*PS_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PS_DEG_RAD;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (toTPA, tol);
+    if (!center) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to solve for TPA center.");
+        psFree (toTPA);
+        psFree (wcs);
+        return NULL;
+    }
+
+    // create wcs transform from toFPA, resulting transformation has units of microns/pixel
+    // adjust wcs transform to use center as reference coordinate
+    psPlaneTransformSetCenter (wcs->trans, toTPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PS_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PS_DEG_RAD;
+
+    // convert wcs->trans to a matrix with units of degrees/pixel
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    psFree (toTPA);
+
+    return wcs;
+}
+
+/* the bilevel astrometry description consists of a polynomial warping from
+   chip coordinates to FPA coordinates (coords->ctype = LIN---WRP), followed
+   by a polynomial representation of the telescope distortion + the projection
+   (coords->ctype = RA---DIS).
+*/
+
+// convert the chip-level toFPA to a wcs polynomial transformation
+pmAstromWCS *pmAstromWCSBilevelChipFromFPA (const pmChip *chip, double tol)
+{
+    // XXX require chip->toFPA->x->nX == chip->toFPA->x->nY
+    // XXX require chip->toFPA->y->nX == chip->toFPA->y->nY
+    // XXX require chip->toFPA->x->nX == chip->toFPA->y->nX
+    // XXX require chip->toFPA->nX == 1,2,3
+
+    // convert chip->toFPA to wcs format (WRP)
+    pmAstromWCS *wcs = pmAstromWCSAlloc(chip->toFPA->x->nX, chip->toFPA->x->nY);
+
+    // Chip to FPA transformation is a Cartesian 'projection'
+    // reference pixel for FPA is 0.0, 0.0
+    wcs->toSky = psProjectionAlloc (0.0, 0.0, 1.0, 1.0, PS_PROJ_WRP);
+    wcs->crval1 = 0.0;
+    wcs->crval2 = 0.0;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (chip->toFPA, tol);
+    if (!center) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to solve for TPA center.");
+        psFree (wcs);
+        return NULL;
+    }
+
+    // adjust wcs transform to use center as reference coordinate
+    // resulting transformation has units of microns/pixel
+    psPlaneTransformSetCenter (wcs->trans, chip->toFPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // output coordinates are in microns : CDELT1,2 has units of microns/pixel
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+// convert the fpa-level toTPA, toSky to a wcs polynomial transformation
+pmAstromWCS *pmAstromWCSBilevelMosaicFromFPA (const pmFPA *fpa, double tol)
+{
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->x->nY
+    // XXX require fpa->toTPA->y->nX == fpa->toTPA->y->nY
+    // XXX require fpa->toTPA->x->nX == fpa->toTPA->y->nX
+    // XXX require fpa->toTPA->nX == 1,2,3
+    // XXX require fpa->toSky->type == PS_PROJ_TAN
+
+    // convert fpa->toTPA + fpa->toSky to wcs format (DIS)
+    pmAstromWCS *wcs = pmAstromWCSAlloc(fpa->toTPA->x->nX, fpa->toTPA->x->nY);
+
+    // convert projection from TPA to SKY into wcs projection (degrees to radians)
+    wcs->toSky = psProjectionAlloc (fpa->toSky->R, fpa->toSky->D, PM_RAD_DEG, PM_RAD_DEG, PS_PROJ_DIS);
+    wcs->crval1 = fpa->toSky->R*PS_DEG_RAD;
+    wcs->crval2 = fpa->toSky->D*PS_DEG_RAD;
+
+    // given transformation, solve for coordinates which yields output coordinates of 0,0
+    psPlane *center = psPlaneTransformGetCenter (fpa->toTPA, tol);
+    if (!center) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to solve for TPA center.");
+        psFree (wcs);
+        return NULL;
+    }
+
+    // adjust wcs transform to use center as reference coordinate
+    // resulting transformation has units of unity (microns/micron)
+    psPlaneTransformSetCenter (wcs->trans, fpa->toTPA, center->x, center->y);
+
+    // calculated center is crpix1,2
+    wcs->crpix1 = center->x;
+    wcs->crpix2 = center->y;
+    psFree (center);
+
+    // pdelt1,2 has units of degrees/micron
+    double pdelt1 = fpa->toSky->Xs * PS_DEG_RAD;
+    double pdelt2 = fpa->toSky->Ys * PS_DEG_RAD;
+
+    // convert wcs->trans to units of degree/micron
+    for (int i = 0; i <= wcs->trans->x->nX; i++) {
+        for (int j = 0; j <= wcs->trans->x->nY; j++) {
+            wcs->trans->x->coeff[i][j] *= pdelt1;
+            wcs->trans->y->coeff[i][j] *= pdelt2;
+        }
+    }
+
+    // cdelt1,2 has units of degrees/micron
+    wcs->cdelt1 = hypot (wcs->trans->x->coeff[1][0], wcs->trans->x->coeff[0][1]);
+    wcs->cdelt2 = hypot (wcs->trans->y->coeff[1][0], wcs->trans->y->coeff[0][1]);
+
+    return wcs;
+}
+
+static psPlaneTransform *
+linearFitToTransform(psPlaneTransform *trans, psRegion *bounds)
+{
+    int     nSamples = 10;  // 10 samples in each dimension
+
+    double   deltaX = (bounds->x1 - bounds->x0);
+    double   deltaY = (bounds->y1 - bounds->y0);
+
+    psArray *src = psArrayAlloc(nSamples * nSamples);
+    psArray *dst = psArrayAlloc(nSamples * nSamples);
+
+    int k=0;
+    for (int j=0; j<nSamples; j++) {
+        double y = j * deltaY / nSamples;
+        for (int i=0; i<nSamples; i++) {
+            psPlane *s = psPlaneAlloc();
+            s->x = i * deltaX / nSamples;
+            s->y = y;
+            psArraySet(src, k, s);
+            psPlane *d = psPlaneTransformApply(NULL, trans, s);
+            psArraySet(dst, k, d);
+            psFree(s);
+            psFree(d);
+            ++k;
+        }
+    }
+
+    psPlaneTransform *newTrans = psPlaneTransformAlloc(1, 1);
+
+    if (!psPlaneTransformFit(newTrans, src, dst, 0, 0)) {
+        psError(PS_ERR_UNKNOWN, false, "linear fit to transform failed");
+        return NULL;
+    }
+
+#ifdef COMPARE_TRANS
+    // compare the computed coordintes from this transform with the original
+    psPlane *new = psPlaneAlloc();
+    for (int i=0; i<psArrayLength(dst); i++) {
+        psPlane *d = (psPlane *) psArrayGet(dst, i);
+        psPlane *s = (psPlane *) psArrayGet(src, i);
+
+        new = psPlaneTransformApply(new, newTrans, s);
+
+        printf("%4d %f %f\n", i, 100.*(new->x - d->x)/d->x, 100.*(new->y - d->y)/d->y);
+    }
+    psFree(new);
+#endif
+    psArrayElementsFree(src);
+    psFree(src);
+    psArrayElementsFree(dst);
+    psFree(dst);
+
+    return newTrans;
+}
+
+bool pmAstromLinearizeTransforms(pmFPA *fpa, pmChip *chip)
+{
+    psRegion    *chipBounds = pmChipPixels(chip);
+
+    psPlaneTransform *newToFPA = linearFitToTransform(chip->toFPA, chipBounds);
+    if (!newToFPA) {
+        psFree(chipBounds);
+        psError(PS_ERR_UNKNOWN, false, "linear fit for toFPA failed");
+        return false;
+    }
+
+    psFree(chip->toFPA);
+    chip->toFPA = newToFPA;
+
+    psFree(chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *chipBounds, 50);
+    if (!chip->fromFPA) {
+        psError(PS_ERR_UNKNOWN, false, "failed to invert linear fit for toFPA");
+        return false;
+    }
+
+    psPlane *chip0 = psPlaneAlloc();
+    chip0->x = 0;
+    chip0->y = 0;
+    psPlane *chip1 = psPlaneAlloc();
+    chip1->x = chipBounds->x1;
+    chip1->y = chipBounds->y1;
+
+    // compute bounding region for fpa
+    psPlane *fpa0 = psPlaneTransformApply(NULL, newToFPA, chip0);
+    psPlane *fpa1 = psPlaneTransformApply(NULL, newToFPA, chip1);
+
+    psRegion *fpaBounds = psRegionAlloc(fpa0->x, fpa1->x, fpa0->y, fpa1->y);
+    psFree(chip0);
+    psFree(chip1);
+    psFree(fpa0);
+    psFree(fpa1);
+
+    psPlaneTransform *newToTPA = linearFitToTransform(fpa->toTPA, fpaBounds);
+    if (!newToTPA) {
+        psError(PS_ERR_UNKNOWN, false, "failed to perform linear fit to toTPA");
+        psFree(fpaBounds);
+        return false;
+    }
+    psFree(fpa->toTPA);
+    fpa->toTPA = newToTPA;
+
+    // XXX: is this region ok?
+    psFree(fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, *fpaBounds, 50);
+    if (!fpa->fromTPA) {
+        psError(PS_ERR_UNKNOWN, false, "failed to invert linear fit to toTPA");
+        return false;
+    }
+
+    fpa->toSky->type = PS_PROJ_TAN;
+
+    psFree(chipBounds);
+    psFree(fpaBounds);
+
+    return true;
+}
+
+static void pmAstromWCSFree (pmAstromWCS *wcs)
+{
+
+    if (!wcs)
+        return;
+    psFree (wcs->trans);
+    psFree (wcs->toSky);
+}
+
+pmAstromWCS *pmAstromWCSAlloc (int nXorder, int nYorder)
+{
+
+    pmAstromWCS *wcs = (pmAstromWCS *) psAlloc(sizeof(pmAstromWCS));
+    psMemSetDeallocator(wcs, (psFreeFunc) pmAstromWCSFree);
+
+    wcs->trans = psPlaneTransformAlloc (nXorder, nYorder);
+    wcs->toSky = NULL;
+
+    memset (wcs->ctype1, 0, PM_ASTROM_WCS_TYPE_SIZE);
+    memset (wcs->ctype2, 0, PM_ASTROM_WCS_TYPE_SIZE);
+    return wcs;
+}
+
+/*****
+
+For mosaic astrometry, we need to have a starting set of projection terms in which the
+chip-to-FPA terms result in a fixed physical unit on the focal plane (eg, pixels or
+microns).  This set of projections, coupled with an identity toTPA (ie, no distortion) will
+result in substantial errors between the observed and predicted star positions on the focal
+plane: this is the measurement of the optical distortion in the camera.  At the same time,
+we need to carry around the transformations which allow us to make an accurate calculation
+of the position of the stars based on the input (per-chip) astrometry.  These
+transformations will allow us to match the raw and ref stars robustly.  To convert the
+per-chip astrometry (which may have been calculated with a different plate scale for each
+chip) to a collection of astrometry terms for chips in a single mosaic, we need to adjust
+the chip-to-FPA scaling (eg, pc11) to match the variations in the effective plate scale for
+each chip (eg, cdelt1).  Thus, we need to carry around both the
+
+*****/
+
+/* discussion of the coord transformations:
+   X,Y: coord on a chip in pixels
+   L,M: coord on the focal plane (pixels)
+   P,Q: coord in the tangent plane (microns or mm?)
+   R,D: coord on the sky
+
+   this function creates WCS terms which convert directly from chip to sky.
+   this function requires a linear, unrotated toTPA distortion term
+   toTPA->x,y->coeff[1][0],[0][1] defines the detector scale (microns / pixel)
+   tpSky->Xs,Ys defines the plate scale (radians / micron)
+*/
+
+/* at this point, we have extracted from the header the WCS terms in the form of a polynomial,
+ * wcs->trans, which will convert X,Y in pixels to L,M in degrees.  we also have the following
+ * elements defined:
+ * type (projection type)
+ * crval1,2 (in RA,DEC degrees)
+ * crpix1,2
+ * cdelt1,2 (in degrees / pixel)
+ * pixelScale (microns / pixel)
+ *
+ * now we convert wcs->trans to toFPA, which is different from wcs->trans in 3 important ways:
+ * 1) the output is in microns (not degrees): divide by cdelt1,2
+ * 2) X,Y are applied directly, without an applied Xo,Yo offset
+ * 3) there is an allowed Lo,Mo term ([0][0] coefficients)
+ */
+
Index: /branches/eam_branch_20090208/psModules/src/extras/.cvsignore
===================================================================
--- /branches/eam_branch_20090208/psModules/src/extras/.cvsignore	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/extras/.cvsignore	(revision 21432)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /branches/eam_branch_20090208/psModules/src/extras/Makefile.am
===================================================================
--- /branches/eam_branch_20090208/psModules/src/extras/Makefile.am	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/extras/Makefile.am	(revision 21432)
@@ -0,0 +1,19 @@
+noinst_LTLIBRARIES = libpsmodulesextras.la
+
+libpsmodulesextras_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesextras_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesextras_la_SOURCES  = \
+	psVectorBracket.c \
+	psPipe.c \
+	psIOBuffer.c \
+	pmKapaPlots.c \
+	pmVisual.c
+
+pkginclude_HEADERS = \
+	psVectorBracket.h \
+	psPipe.h \
+	psIOBuffer.h \
+	pmKapaPlots.h \
+	pmVisual.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20090208/psModules/src/extras/pmVisual.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/extras/pmVisual.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/extras/pmVisual.c	(revision 21432)
@@ -0,0 +1,327 @@
+/** The following are a collection of core procedures to aid the creation of visaual diagnostics
+ *  @author Chris Beaumont, IfA
+ *  @date January 23, 2008
+ **/
+
+/* Include Files  */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+#include "pmFPAExtent.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+# include "pmVisual.h"
+# include "pmKapaPlots.h"
+# define KAPAX 700
+# define KAPAY 700
+
+
+bool pmVisualInitWindow (int *kapid, char *name) {
+    if (*kapid == -1) {
+        *kapid = KapaOpenNamedSocket("kapa", name);
+        if (*kapid == -1) {
+            fprintf (stderr, "Failure to open kapa.\n");
+            return false;
+        }
+        KapaResize (*kapid, KAPAX, KAPAY);
+    }
+    return true;
+}
+
+
+bool pmVisualInitGraph (int kapa, KapaSection *section, Graphdata *graphdata)
+{
+    KapaSetSection (kapa, section);
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaSetLimits (kapa, graphdata);
+    KapaBox (kapa, graphdata);
+    return true;
+}
+
+
+bool pmVisualAskUser(bool *plotFlag, bool *packageFlag)
+{
+    char key[10];
+    fprintf (stdout, "[c]ontinue? [s]kip the rest of these plots? [a]bort all visual plots?");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    }
+    if (key[0] == 's') {
+        *plotFlag = false;
+    }
+    if (key[0] == 'a') {
+        *packageFlag = false;
+    }
+    return true;
+}
+
+
+bool pmVisualImStats(psImage *image, double *mean, double *stdev, double *min, double *max) {
+
+    *min = +FLT_MAX;
+    *max = -FLT_MAX;
+    double ex = 0;  // <x>
+    double ex2 = 0; // <x^2>
+    int numPix = 0; // number of finite pixels
+    bool isDouble = image->type.type == PS_TYPE_F64;
+
+    if(!isDouble && (image->type.type != PS_TYPE_F32)) {
+        fprintf(stderr, "Image must be PS_TYPE_F32 or PS_TYPE_F64\n");
+        return false;
+    }
+
+    for(int i = 0; i < image->numRows; i++) {
+        for(int j = 0; j < image->numCols; j++) {
+            double entry;
+            if(isDouble) {
+                if (!isfinite(image->data.F64[i][j])) continue;
+                entry = image->data.F64[i][j];
+            } else {
+                if (!isfinite(image->data.F32[i][j])) continue;
+                entry = image->data.F32[i][j];
+            }
+            numPix++;
+            ex += entry;
+            ex2 += (entry * entry);
+            *min = PS_MIN(*min, entry);
+            *max = PS_MAX(*max, entry);
+        }
+    }
+
+    if (numPix == 0) return false;
+    ex /= numPix;
+    ex2 /= numPix;
+    *mean = ex;
+    *stdev = sqrt(ex2  - ex * ex);
+
+    return true;
+}
+
+
+bool pmVisualTriplePlot (int kapid, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+    pmVisualScaleGraphdata (graphdata, xVec, yVec, true);
+    //printf("%f %f %f %f \n",graphdata->xmin, graphdata->xmax, graphdata->ymin, graphdata->ymax);
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+    pmVisualCreateScaleVec (zVec, zScale, increasing);
+
+    KapaSetFont (kapid, "helvetica", 14);
+    KapaSetLimits(kapid, graphdata);
+    KapaBox (kapid, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->ptype = 7;
+    graphdata->style = 2;
+    graphdata->size = -1;
+    KapaPrepPlot (kapid, xVec->n, graphdata);
+    KapaPlotVector (kapid, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapid, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapid, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+
+bool pmVisualTripleOverplot (int kapid, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing) {
+
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+    pmVisualCreateScaleVec (zVec, zScale, increasing);
+
+    KapaSetFont (kapid, "helvetica", 14);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapid, xVec->n, graphdata);
+    KapaPlotVector (kapid, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapid, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapid, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing) {
+    // set limits based on data values
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+
+    for (int i = 0; i < zVec->n; i++) {
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    float range = zmax - zmin;
+    if (range == 0.0) {
+        psVectorInit (zScale, 1.0);
+    } else {
+        for (int i = 0; i < zVec->n; i++) {
+            if (increasing) {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zVec->data.F32[i] - zmin)/range));
+            } else {
+                zScale->data.F32[i] = PS_MIN (1.5, PS_MAX(0.05, 1.5*(zmax - zVec->data.F32[i])/range));
+            }
+        }
+    }
+    return true;
+}
+
+
+bool pmVisualScaleImage(int kapaFD, psImage *inImage, const char *name, int channel, bool clip) {
+
+    KiiImage image;
+    KapaImageData data;
+    Coords coords;
+
+    //make sure we have a compatible image type
+    if(inImage->type.type != PS_TYPE_F32) {
+        fprintf(stderr, "Cannot display this image (imcompatible data type)\n");
+        return false;
+    }
+
+    strcpy (coords.ctype, "RA---TAN");
+
+    double min, max, stdev, mean;
+    if(!pmVisualImStats(inImage, &mean, &stdev, &min, &max)) return false;
+
+    image.data2d = inImage->data.F32;
+    image.Nx = inImage->numCols;
+    image.Ny = inImage->numRows;
+    strcpy (data.name, name);
+    strcpy (data.file, name);
+
+    data.zero = clip ? mean - 3 * stdev : min;
+    data.range = clip ? 6 * stdev : max - min;
+    data.logflux = 0;
+
+    KiiSetChannel (kapaFD, channel);
+    KiiNewPicture2D (kapaFD, &image, &data, &coords);
+
+    return true;
+}
+
+
+psImage* pmVisualImageToFloat(psImage *image) {
+    psImage *result = psImageAlloc(image->numCols, image->numRows, PS_TYPE_F32);
+
+    if (image->type.type == PS_TYPE_F32) {
+        psImageOverlaySection(result, image, 0, 0, "=");
+        return image;
+    } else if (image->type.type == PS_TYPE_F64) {
+        for (int i = 0; i < image->numRows; i++) {
+            for (int j = 0; j < image->numCols; j++) {
+                result->data.F32[i][j] = (float) image->data.F64[i][j];
+            }
+        }
+    } else {
+        fprintf(stderr, "Unsupported psImage data type (F32 or F64)");
+        return NULL;
+    }
+    return result;
+}
+
+
+bool pmVisualScaleGraphdata(Graphdata *graphdata, psVector *xVec, psVector *yVec, bool clip) {
+
+    graphdata->xmin = +FLT_MAX;
+    graphdata->xmax = -FLT_MAX;
+    graphdata->ymin = +FLT_MAX;
+    graphdata->ymax = -FLT_MAX;
+
+    //determine standard deviation of xVec and yVec
+    psStats *statsX = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psStats *statsY = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psVectorStats (statsX, xVec, NULL, NULL, 0);
+    psVectorStats (statsY, yVec, NULL, NULL, 0);
+
+    float xhi  = statsX->sampleMedian + 3 *statsX->sampleStdev;
+    float xlo = statsX->sampleMedian - 3 *statsX->sampleStdev;
+    float yhi = statsY->sampleMedian + 3 *statsY->sampleStdev;
+    float ylo = statsY->sampleMedian - 3 *statsY->sampleStdev;
+
+    // don't sigma clip
+    if (!clip) {
+        xhi = +FLT_MAX;
+        xlo = -FLT_MAX;
+        yhi = +FLT_MAX;
+        ylo = -FLT_MAX;
+    }
+
+    for(int i = 0; i < xVec->n; i++) {
+        if (!isfinite(xVec->data.F32[i])) continue;
+        if (xVec->data.F32[i] > xhi || xVec->data.F32[i] < xlo) continue;
+        graphdata->xmin = PS_MIN (graphdata->xmin, xVec->data.F32[i]);
+        graphdata->xmax = PS_MAX (graphdata->xmax, xVec->data.F32[i]);
+    }
+
+    for (int i = 0; i < yVec->n; i++) {
+        if (!isfinite(xVec->data.F32[i])) continue;
+        if (yVec->data.F32[i] > yhi || yVec->data.F32[i] < ylo) continue;
+        graphdata->ymin = PS_MIN (graphdata->ymin, yVec->data.F32[i]);
+        graphdata->ymax = PS_MAX (graphdata->ymax, yVec->data.F32[i]);
+    }
+
+
+    // abort if there is no good data
+    if (!isfinite(xhi) || !isfinite(xlo) || !isfinite(yhi) || !isfinite(ylo)) {
+        graphdata->xmin = -1;
+        graphdata->ymin  = -1;
+        graphdata->xmax = 1;
+        graphdata->ymax = 1;
+        psFree(statsX);
+        psFree(statsY);
+        return false;
+    }
+
+    // add a whitespace border
+    float range = graphdata->xmax - graphdata->xmin;
+    if (range == 0) range = 1;
+    graphdata->xmin -= .05 * range;
+    graphdata->xmax += .05 * range;
+
+    range = graphdata->ymax - graphdata->ymin;
+    if (range == 0) range = 1;
+    graphdata->ymin -= .05 * range;
+    graphdata->ymax += .05 * range;
+
+    psFree (statsX);
+    psFree (statsY);
+    return true;
+}
+
+#else
+
+bool pmVisualInitWindow(int *kapid, char *name){return true;}
+bool pmVisualInitGraph (int kapa, void *section, void *graphdata){return true;}
+
+bool pmVisualAskUser(bool *plotFlag, bool *packageFlag){return true;}
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max){return true;}
+bool pmVisualTriplePlot (int kapid, void *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing){return true;}
+bool pmVisualTripleOverplot (int kapid, void *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing){return true;}
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing){return true;}
+bool pmVisualResidPlot (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe, char *title, int *kapa, int *kapa2){return true;}
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip){return true;}
+bool pmVisualScaleGraphdata(void *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip){return true;}
+
+psImage* pmVisualImageToFloat(psImage *image){return NULL;}
+
+
+#endif
Index: /branches/eam_branch_20090208/psModules/src/extras/pmVisual.h
===================================================================
--- /branches/eam_branch_20090208/psModules/src/extras/pmVisual.h	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/extras/pmVisual.h	(revision 21432)
@@ -0,0 +1,137 @@
+/* @file pmVisual.h
+ * @brief functions to create visual diagnostics with the help of 'kapa'
+ * @author Chris Beaumont, IfA
+ *
+ * Copyright 2009 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_VISUAL_H
+#define PM_VISUAL_H
+
+#if (HAVE_KAPA)
+
+
+/** Open, name, and resize a window for plotting.
+ * @param kapid an identifier for this window. Initialize to -1. Its value will be updated.
+ * @param name What to name the window. Seems not to like spaces.
+ * @return true for successful completion
+*/
+bool pmVisualInitWindow (int *kapid, char *name);
+
+
+/** Initialize a graph, set the appropriate section and font.
+ * @param kapa the index of the kapa window to plot to
+ * @param section the section to use
+ * @graphdata to use
+ * @return true for successful completion
+ */
+bool pmVisualInitGraph (int kapa, KapaSection *section, Graphdata *graphdata);
+
+
+/** Ask the user how to proceed.
+ * At the user's request, this will disable diagnostic plotting.
+ * @param plotFlag, set to false if this plot should be disabled in the future
+ * @param packageFlag, set to false if all plots from this package (e.g. psastro, pmSubtraction)
+ *                      should be disabled.
+ */
+bool pmVisualAskUser(bool *plotFlag, bool *packageFlag);
+
+
+/** Scale and display an image.
+ * @param kapaFD the index of the Kapa window to draw to
+ * @param inImage the image to display
+ * @param name the image label
+ * @param which channel to draw to (zero works)
+ * @param clip set to true to sigma clip the image when scaling
+ * @return true for successful completion */
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip);
+
+
+/** Calculate statistics on an image.
+ *  This can handle non-finite input pixels
+ *  @param image to calculate statistics for
+ *  @param mean  stores the calculated mean
+ *  @param stdev stores the calculated standard deviation
+ *  @param min stores the calculated minimum
+ *  @param max stores the calculated maximum
+ *  @return true for successful completion */
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max);
+
+
+/** Create a PS_TYPE_F32 image out of a PS_TYPE_F32 or PS_TYPE_F64 image.
+ * This is needed when displaying PS_TYPE_F64 images, which will not work wtih Kii.
+ * @param image to convert to type PS_TYPE_F32
+ * @return a copy of the image of type PS_TYPE_F32, or NULL upon failure */
+psImage* pmVisualImageToFloat(psImage *image);
+
+
+/** Create a scaled vector of plot point sizes from an unscaled, raw vector.
+ *  Used for input into pmVisualTriplePlot
+ * @param zVec the raw data
+ * @param zScale the scaled vector of plot point sizes
+ * @return true for successful completion
+ */
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing);
+
+
+/** Use x and y data to determine appropriate values for a Graphdata structure.
+ * This procedure sets the max and min keywords of a Graphdata structure to encompass
+ * the data coordinates given in xVec and yVec. Optionally, it will try to ignore outliers.
+ * @param graphdata structure to set up
+ * @param xVec X coordinates of data points
+ * @param yVec Y coordinates of data points
+ * @clip  set to true to attempt to clip out outliers
+ * @return true for successful completion
+ */
+bool pmVisualScaleGraphdata(Graphdata *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip);
+
+
+/** Create a scatter plot form a set of (x,y) positions, whose plot points
+ *  are scaled by the size of a z vector
+ * @param kapid the index of the kapa window to plot to
+ * @param graphdata describing the plotting window
+ * @param xVec x positions
+ * @param yVec y positions
+ * @param zVec plot point sizes
+ * @param increasing whether the points in x,y,zvec are sorted
+ */
+bool pmVisualTriplePlot (int kapid, Graphdata *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing);
+
+/** The same as pmVisualTriplePlot, but draws new points without erasing the old plot window
+ * @param kapid the index of the kapa window to plot to
+ * @param xVec x positions
+ * @param yVec y positions
+ * @param zVec plot point sizes
+ * @param increasing whether the x,y,zvectors are listed in increasing order
+ */
+bool pmVisualTripleOverplot (int kapid, Graphdata *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing);
+
+#else
+
+// kapa-specific data types are changed to void
+bool pmVisualInitWindow (int *kapid, char *name);
+bool pmVisualInitGraph (int kapa, void *section, void *graphdata);
+bool pmVisualAskUser(bool *plotFlag, bool *packageFlag);
+bool pmVisualScaleImage(int kapaFD, psImage *inImage,
+                        const char *name, int channel, bool clip);
+bool pmVisualImStats(psImage *image, double *mean,
+                     double *stdev, double *min, double *max);
+psImage* pmVisualImageToFloat(psImage *image);
+bool pmVisualCreateScaleVec (psVector *zVec, psVector *zScale, bool increasing);
+bool pmVisualResidPlot (psArray *rawstars, psArray *refstars, psArray *match,
+                        psMetadata *recipe, char *title, int *kapa, int *kapa2);
+bool pmVisualScaleGraphdata(void *graphdata, psVector *xVec,
+                            psVector *yVec, bool clip);
+bool pmVisualTriplePlot (int kapid, void *graphdata, psVector *xVec,
+                         psVector *yVec, psVector *zVec, bool increasing);
+bool pmVisualTripleOverplot (int kapid, void *graphdata, psVector *xVec,
+                             psVector *yVec, psVector *zVec, bool increasing);
+
+#endif //HAVE_KAPA
+
+#endif //ndef PM_VISUAL_H
Index: /branches/eam_branch_20090208/psModules/src/imcombine/Makefile.am
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/Makefile.am	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/Makefile.am	(revision 21432)
@@ -0,0 +1,41 @@
+noinst_LTLIBRARIES = libpsmodulesimcombine.la
+
+libpsmodulesimcombine_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS)
+libpsmodulesimcombine_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+
+libpsmodulesimcombine_la_SOURCES = \
+	pmReadoutCombine.c	\
+	pmStack.c		\
+	pmStackReject.c		\
+	pmSubtraction.c		\
+	pmSubtractionAnalysis.c	\
+	pmSubtractionEquation.c	\
+	pmSubtractionIO.c	\
+	pmSubtractionKernels.c	\
+	pmSubtractionMask.c	\
+	pmSubtractionMatch.c	\
+	pmSubtractionParams.c	\
+	pmSubtractionStamps.c	\
+	pmSubtractionThreads.c	\
+	pmPSFEnvelope.c         \
+	pmSubtractionVisual.c
+
+pkginclude_HEADERS = \
+	pmImageCombine.h \
+	pmReadoutCombine.h	\
+	pmStack.h		\
+	pmStackReject.h		\
+	pmSubtraction.h		\
+	pmSubtractionAnalysis.h	\
+	pmSubtractionEquation.h	\
+	pmSubtractionIO.h	\
+	pmSubtractionKernels.h	\
+	pmSubtractionMask.h	\
+	pmSubtractionMatch.h	\
+	pmSubtractionParams.h	\
+	pmSubtractionStamps.h	\
+	pmSubtractionThreads.h	\
+	pmPSFEnvelope.h         \
+	pmSubtractionVisual.h
+
+CLEANFILES = *~
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtraction.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtraction.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtraction.c	(revision 21432)
@@ -0,0 +1,1391 @@
+/** @file pmSubtraction.c
+ *
+ *  @author Paul Price, IfA
+ *  @author GLG, MHPCC
+ *
+ *  Copyright 2004-2007 Institute for Astronomy, University of Hawaii
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"                      // Required for pmFPA.h
+#include "pmFPA.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionThreads.h"
+
+#include "pmSubtraction.h"
+
+//#define TESTING
+
+#define PIXEL_LIST_BUFFER 100           // Number of entries to add to pixel list at a time
+#define MIN_SAMPLE_STATS    7           // Minimum number to use sample statistics; otherwise use quartiles
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Generate the kernel to apply to the variance from the normal kernel
+static psKernel *varianceKernel(psKernel *out, // Output kernel
+                                psKernel *normalKernel // Normal kernel
+                                )
+{
+    // Kernel range
+    int xMin = normalKernel->xMin, xMax = normalKernel->xMax;
+    int yMin = normalKernel->yMin, yMax = normalKernel->yMax;
+
+    if (!out) {
+        out = psKernelAlloc(xMin, xMax, yMin, yMax);
+    }
+
+    // Take the square of the normal kernel
+    double sumNormal = 0.0, sumVariance = 0.0; // Sum of the normal and variance kernels
+    for (int v = yMin; v <= yMax; v++) {
+        for (int u = xMin; u <= xMax; u++) {
+            float value = normalKernel->kernel[v][u]; // Value of interest
+            float value2 = PS_SQR(value); // Value squared
+            sumNormal += value;
+            sumVariance += value2;
+            out->kernel[v][u] = value2;
+        }
+    }
+
+    // Normalise so that the sum of the variance kernel is the square of the sum of the normal kernel
+    // This is required to keep the relative scaling between the image and the variance map
+    psBinaryOp(out->image, out->image, "*", psScalarAlloc(1.0 / sumVariance, PS_TYPE_F32));
+
+    return out;
+}
+
+// Generate an image of the solved kernel
+static psKernel *solvedKernel(psKernel *kernel, // Kernel, to return
+                              const pmSubtractionKernels *kernels, // Kernel basis functions
+                              const psImage *polyValues, // Spatial polynomial values
+                              bool wantDual // Want the dual (second) kernel?
+                              )
+{
+    assert(kernels);
+    assert(polyValues);
+
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int size = kernels->size;           // Kernel half-size
+    if (!kernel) {
+        kernel = psKernelAlloc(-size, size, -size, size);
+    }
+    psImageInit(kernel->image, 0.0);
+
+    for (int i = 0; i < numKernels; i++) {
+        double value = p_pmSubtractionSolutionCoeff(kernels, polyValues, i, wantDual); // Polynomial value
+
+        switch (kernels->type) {
+          case PM_SUBTRACTION_KERNEL_POIS: {
+              int u = kernels->u->data.S32[i]; // Offset in x
+              int v = kernels->v->data.S32[i]; // Offset in y
+              kernel->kernel[v][u] += value;
+              kernel->kernel[0][0] -= value;
+              break;
+          }
+          /* SPAM and FRIES use the same method */
+          case PM_SUBTRACTION_KERNEL_SPAM:
+          case PM_SUBTRACTION_KERNEL_FRIES: {
+              int uStart = kernels->u->data.S32[i];
+              int uStop = kernels->uStop->data.S32[i];
+              int vStart = kernels->v->data.S32[i];
+              int vStop = kernels->vStop->data.S32[i];
+
+              // Normalising sum of kernel component to unity
+              float norm = 1.0 / (float)((uStop - uStart + 1) * (vStop - vStart + 1));
+
+              for (int v = vStart; v <= vStop; v++) {
+                  for (int u = uStart; u <= uStop; u++) {
+                      kernel->kernel[v][u] += norm * value;
+                      kernel->kernel[0][0] -= value;
+                  }
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_GUNK: {
+              if (i < kernels->inner) {
+                  // Using pre-calculated function
+                  psKernel *preCalc = kernels->preCalc->data[i]; // Precalculated values
+                  // Iterating over the kernel
+                  for (int v = -size; v <= size; v++) {
+                      for (int u = -size; u <= size; u++) {
+                          kernel->kernel[v][u] += preCalc->kernel[v][u] * value;
+                          // Photometric scaling is built into the preCalc kernel --- no subtraction!
+                      }
+                  }
+              } else {
+                  // Using delta function
+                  int u = kernels->u->data.S32[i]; // Offset in x
+                  int v = kernels->v->data.S32[i]; // Offset in y
+                  kernel->kernel[v][u] += value;
+                  kernel->kernel[0][0] -= value;
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_ISIS: {
+              psArray *preCalc = kernels->preCalc->data[i]; // Precalculated values
+              psVector *xKernel = preCalc->data[0]; // Kernel in x
+              psVector *yKernel = preCalc->data[1]; // Kernel in y
+              // Iterating over the kernel
+              for (int y = 0, v = -size; v <= size; y++, v++) {
+                  for (int x = 0, u = -size; u <= size; x++, u++) {
+                      kernel->kernel[v][u] +=  value * xKernel->data.F32[x] * yKernel->data.F32[y];
+                  }
+              }
+              // Photometric scaling for even kernels only
+              if (kernels->u->data.S32[i] % 2 == 0 && kernels->v->data.S32[i] % 2 == 0) {
+                  kernel->kernel[0][0] -= value;
+              }
+              break;
+          }
+          case PM_SUBTRACTION_KERNEL_RINGS: {
+              psArray *preCalc = kernels->preCalc->data[i]; // Precalculated data
+              psVector *uCoords = preCalc->data[0]; // u coordinates
+              psVector *vCoords = preCalc->data[1]; // v coordinates
+              psVector *poly = preCalc->data[2]; // Polynomial values
+              int num = uCoords->n;     // Number of pixels
+
+              for (int j = 0; j < num; j++) {
+                  int u = uCoords->data.S32[j], v = vCoords->data.S32[j]; // Kernel coordinates
+                  kernel->kernel[v][u] += poly->data.F32[j] * value;
+              }
+              // Photometric scaling is built into the kernel --- no subtraction!
+              break;
+          }
+          default:
+            psAbort("Should never get here.");
+        }
+    }
+
+    // Put in the normalisation component
+    kernel->kernel[0][0] += (wantDual ? 1.0 : p_pmSubtractionSolutionNorm(kernels));
+
+    return kernel;
+}
+
+// Subtract the (0,0) element to preserve photometric scaling
+static void convolveSub(psKernel *convolved, // Convolved image
+                        const psKernel *image, // Image being convolved
+                        int footprint  // Size of region of interest
+                        )
+{
+    // Can't use psBinaryOp because the images are of different size
+    for (int y = -footprint; y <= footprint; y++) {
+        for (int x = -footprint; x <= footprint; x++) {
+            convolved->kernel[y][x] -= image->kernel[y][x];
+        }
+    }
+    return;
+}
+
+// Generate the convolution given some offset
+static psKernel *convolveOffset(const psKernel *image, // Image to convolve (a kernel for convenience)
+                                int u, int v, // Offset to apply
+                                int footprint // Size of region of interest
+                                )
+{
+    psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Convolved image
+    int numBytes = (2 * footprint + 1) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+    for (int y = -footprint; y <= footprint; y++) {
+        // Convolution with a delta function is just the value specified by the offset
+        memcpy(&convolved->kernel[y][-footprint], &image->kernel[y - v][-footprint - u], numBytes);
+    }
+    return convolved;
+}
+
+// Take a subset of an image
+static inline psImage *convolveSubsetAlloc(psImage *image, // Image to be convolved
+                                           psRegion region, // Region of interest (with border)
+                                           bool threaded // Are we running threaded?
+                                           )
+{
+    if (!image) {
+        return NULL;
+    }
+    // XXX if (threaded) {
+    // XXX     psMutexLock(image);
+    // XXX }
+    psImage *subset = psImageSubset(image, region); // Subset image, to return
+    // XXX if (threaded) {
+    // XXX     psMutexUnlock(image);
+    // XXX }
+    return subset;
+}
+
+// Free the subset of an image
+static inline void convolveSubsetFree(psImage *parent, // Parent image
+                                      psImage *child, // Child (subset) image
+                                      bool threaded // Are we running threaded?
+                                      )
+{
+    if (!child || !parent) {
+        return;
+    }
+    // XXX if (threaded) {
+    // XXX     psMutexLock(parent);
+    // XXX }
+    psFree(child);
+    // XXX if (threaded) {
+    // XXX     psMutexUnlock(parent);
+    // XXX }
+    return;
+}
+
+// Convolve an image using FFT
+static void convolveFFT(psImage *target,// Place the result in here
+                        psImage *image, // Image to convolve
+                        psImage *mask, // Mask image
+                        psImageMaskType maskVal, // Value to mask
+                        const psKernel *kernel, // Kernel by which to convolve
+                        psRegion region,// Region of interest
+                        float background, // Background to add
+                        int size        // Size of (square) kernel
+                        )
+{
+    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
+                                  region.y0 - size, region.y1 + size); // Add a border
+
+    bool threaded = pmSubtractionThreaded(); // Are we running threaded?
+
+    psImage *subImage = convolveSubsetAlloc(image, border, threaded); // Subimage to convolve
+    psImage *subMask = convolveSubsetAlloc(mask, border, threaded); // Subimage mask
+
+    psImage *convolved = psImageConvolveFFT(NULL, subImage, subMask, maskVal, kernel); // Convolution
+
+    convolveSubsetFree(image, subImage, threaded);
+    convolveSubsetFree(mask, subMask, threaded);
+
+    // Now, we have to stick it in where it belongs
+    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
+    if (background != 0.0) {
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
+                target->data.F32[yTarget][xTarget] = convolved->data.F32[ySource][xSource] + background;
+            }
+        }
+    } else {
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            memcpy(&target->data.F32[yTarget][xMin], &convolved->data.F32[ySource][size], numBytes);
+        }
+    }
+    psFree(convolved);
+
+    return;
+}
+
+
+// Convolve an image using FFT
+static void convolveVarianceFFT(psImage *target,// Place the result in here
+                              psImage *variance, // Variance map to convolve
+                              psImage *sys, // Systematic error image
+                              psImage *mask, // Mask image
+                              psImageMaskType maskVal, // Value to mask
+                              const psKernel *kernel, // Kernel by which to convolve
+                              psRegion region,// Region of interest
+                              int size        // Size of (square) kernel
+                              )
+{
+    psRegion border = psRegionSet(region.x0 - size, region.x1 + size,
+                                  region.y0 - size, region.y1 + size); // Add a border
+
+    bool threaded = pmSubtractionThreaded(); // Are we running threaded?
+
+    psImage *subVariance = convolveSubsetAlloc(variance, border, threaded); // Variance map
+    psImage *subSys = convolveSubsetAlloc(sys, border, threaded); // Systematic error image
+    psImage *subMask = convolveSubsetAlloc(mask, border, threaded); // Mask
+
+    // XXX Can trim this a little by combining the convolution: only have to take the FFT of the kernel once
+    psImage *convVariance = psImageConvolveFFT(NULL, subVariance, subMask, maskVal, kernel); // Convolved variance
+    psImage *convSys = subSys ? psImageConvolveFFT(NULL, subSys, subMask, maskVal, kernel) : NULL; // Conv sys
+
+    convolveSubsetFree(variance, subVariance, threaded);
+    convolveSubsetFree(sys, subSys, threaded);
+    convolveSubsetFree(mask, subMask, threaded);
+
+    // Now, we have to stick it in where it belongs
+    int xMin = region.x0, xMax = region.x1, yMin = region.y0, yMax = region.y1; // Bounds of region
+    if (convSys) {
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            for (int xTarget = xMin, xSource = size; xTarget < xMax; xTarget++, xSource++) {
+                target->data.F32[yTarget][xTarget] = convVariance->data.F32[ySource][xSource] +
+                    convSys->data.F32[ySource][xSource];
+            }
+        }
+    } else {
+        int numBytes = (xMax - xMin) * PSELEMTYPE_SIZEOF(PS_TYPE_F32); // Number of bytes to copy
+        for (int yTarget = yMin, ySource = size; yTarget < yMax; yTarget++, ySource++) {
+            memcpy(&target->data.F32[yTarget][xMin], &convVariance->data.F32[ySource][size], numBytes);
+        }
+    }
+
+    psFree(convVariance);
+    psFree(convSys);
+
+    return;
+}
+
+
+// Convolve an image directly
+static void convolveDirect(psImage *target, // Put the result here
+                           const psImage *image, // Image to convolve
+                           const psKernel *kernel, // Kernel by which to convolve
+                           psRegion region,// Region of interest
+                           float background, // Background to add
+                           int size        // Size of (square) kernel
+                           )
+{
+    for (int y = region.y0; y < region.y1; y++) {
+        for (int x = region.x0; x < region.x1; x++) {
+            target->data.F32[y][x] = background;
+            for (int v = -size; v <= size; v++) {
+                for (int u = -size; u <= size; u++) {
+                    target->data.F32[y][x] += kernel->kernel[v][u] * image->data.F32[y - v][x - u];
+                }
+            }
+        }
+    }
+    return;
+}
+
+// Convolve a region of an image
+static inline void convolveRegion(psImage *convImage, // Convolved image (output)
+                                  psImage *convVariance, // Convolved variance map (output), or NULL
+                                  psImage *convMask, // Convolve mask (output), or NULL
+                                  psKernel **kernelImage, // Convolution kernel for the image
+                                  psKernel **kernelVariance, // Convolution kernel for the variance map, or NULL
+                                  psImage *image, // Image to convolve
+                                  psImage *variance, // Variance map to convolve, or NULL
+                                  psImage *sys, // Systematic error image, or NULL
+                                  psImage *subMask, // Subtraction mask
+                                  const pmSubtractionKernels *kernels, // Kernels
+                                  const psImage *polyValues, // Polynomial values
+                                  float background, // Background value to apply
+                                  psRegion region, // Region to convolve
+                                  psImageMaskType maskBad, // Value to give bad pixels
+                                  psImageMaskType maskPoor, // Value to give poor pixels
+                                  float poorFrac, // Fraction for "poor"
+                                  bool useFFT,  // Use FFT to convolve?
+                                  bool wantDual // Want the dual convolution?
+    )
+{
+    *kernelImage = solvedKernel(*kernelImage, kernels, polyValues, wantDual);
+    if (variance || subMask) {
+        *kernelVariance = varianceKernel(*kernelVariance, *kernelImage);
+    }
+
+    psImageMaskType subBad;                  // Bad pixels in subtraction mask
+    psImageMaskType subConvBad;              // Bad pixels in subtraction mask when convolving
+    psImageMaskType subConvPoor;             // Poor pixels in subtraction mask when convolving
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || (kernels->mode == PM_SUBTRACTION_MODE_DUAL && !wantDual)) {
+        subBad = PM_SUBTRACTION_MASK_BAD_1;
+        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_1;
+        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_1;
+    } else {
+        subBad = PM_SUBTRACTION_MASK_BAD_2;
+        subConvBad = PM_SUBTRACTION_MASK_CONVOLVE_BAD_2;
+        subConvPoor = PM_SUBTRACTION_MASK_CONVOLVE_2;
+    }
+
+    // Convolve the image and variance
+    if (useFFT) {
+        // Use Fast Fourier Transform to do the convolution
+        // This provides a big speed-up for large kernels
+        convolveFFT(convImage, image, subMask, subBad, *kernelImage, region, background, kernels->size);
+        if (variance) {
+            convolveVarianceFFT(convVariance, variance, sys, subMask, subBad, *kernelVariance, region, kernels->size);
+        }
+    } else {
+        // XXX Direct convolution doesn't account for bad pixels yet
+        convolveDirect(convImage, image, *kernelImage, region, background, kernels->size);
+        if (variance) {
+            convolveDirect(convVariance, variance, *kernelVariance, region, 0.0, kernels->size);
+        }
+    }
+
+    // Convolve the mask for bad pixels
+    if (subMask && convMask) {
+        int box = p_pmSubtractionBadRadius(*kernelImage, kernels, polyValues,
+                                           wantDual, poorFrac); // Size of bad box
+        if (box > 0) {
+            int colMin = region.x0, colMax = region.x1, rowMin = region.y0, rowMax = region.y1; // Bounds
+            bool threaded = pmSubtractionThreaded(); // Are we running threaded?
+
+            psRegion region = psRegionSet(colMin - box, colMax + box,
+                                          rowMin - box, rowMax + box); // Region to convolve
+
+            psImage *image = convolveSubsetAlloc(subMask, region, threaded); // Mask to convolve
+
+            psImage *convolved = psImageConvolveMask(NULL, image, subBad, subConvBad,
+                                                     -box, box, -box, box); // Convolved subtraction mask
+
+            convolveSubsetFree(subMask, image, threaded);
+
+            psAssert(convolved->numCols - 2 * box == colMax - colMin, "Bad number of columns");
+            psAssert(convolved->numRows - 2 * box == rowMax - rowMin, "Bad number of rows");
+
+            for (int yTarget = rowMin, ySource = box; yTarget < rowMax; yTarget++, ySource++) {
+                // Dereference images
+                psImageMaskType *target = &convMask->data.PS_TYPE_IMAGE_MASK_DATA[yTarget][colMin]; // Target values
+                psImageMaskType *source = &convolved->data.PS_TYPE_IMAGE_MASK_DATA[ySource][box]; // Source values
+                for (int xTarget = colMin; xTarget < colMax; xTarget++, target++, source++) {
+                    if (*source & subConvBad) {
+                        *target |= maskBad;
+                    } else if (*source & subConvPoor) {
+                        *target |= maskPoor;
+                    }
+                }
+            }
+
+            // No need to lock: we own this
+            psFree(convolved);
+        }
+    }
+
+    return;
+}
+
+// Generate an image that can be used to track systematic errors
+static psImage *subtractionSysErrImage(const psImage *image, // Image from which to make sys err image
+                                       float sysError // Relative systematic error
+                                       )
+{
+    if (!isfinite(sysError) || sysError == 0.0) {
+        return NULL;
+    }
+
+    int numCols = image->numCols, numRows = image->numRows; // Size of image
+    psImage *sys = psImageAlloc(numCols, numRows, PS_TYPE_F32); // Systematic error image
+
+    float sysError2 = PS_SQR(sysError); // Square of the systematic error
+    for (int y = 0; y < numRows; y++) {
+        for (int x = 0; x < numCols; x++) {
+            sys->data.F32[y][x] = PS_SQR(image->data.F32[y][x]) * sysError2;
+        }
+    }
+
+    return sys;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+psKernel *p_pmSubtractionConvolveStampPrecalc(const psKernel *image, const psKernel *kernel)
+{
+    PS_ASSERT_KERNEL_NON_NULL(image, NULL);
+    PS_ASSERT_KERNEL_NON_NULL(kernel, NULL);
+
+    psImage *conv = psImageConvolveFFT(NULL, image->image, NULL, 0, kernel); // Convolved image
+    int x0 = - image->xMin, y0 = - image->yMin; // Position of centre of convolved image
+    psKernel *convolved = psKernelAllocFromImage(conv, x0, y0); // Kernel version
+    psFree(conv);
+    return convolved;
+}
+
+int p_pmSubtractionBadRadius(psKernel *preKernel, const pmSubtractionKernels *kernels,
+                             const psImage *polyValues, bool wantDual, float poorFrac)
+{
+    psKernel *kernel;                   // Kernel to use
+    if (!preKernel) {
+        kernel = solvedKernel(NULL, kernels, polyValues, wantDual);
+    } else {
+        kernel = psMemIncrRefCounter(preKernel);
+    }
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, -1);
+
+    int xMin = kernel->xMin, xMax = kernel->xMax, yMin = kernel->yMin, yMax = kernel->yMax; // Bounds
+
+    // Determine the threshold between bad and poor
+    double sumKernel2 = 0.0;            // Sum of the kernel-squared
+    for (int y = yMin; y <= yMax; y++) {
+        for (int x = xMin; x <= xMax; x++) {
+            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
+        }
+    }
+    float threshold = sumKernel2 * poorFrac; // Threshold between poor and bad
+
+    // Get bounds of threshold region
+    // Start with the entire kernel, and keep reducing the size of the box until it sum goes above threshold
+    int box = kernels->size;            // Size of box with bad pixels
+    for (double sumBox = 0.0; sumBox < threshold && box > 0; box--) {
+        for (int x = -box; x <= box; x++) {
+            sumBox += PS_SQR(kernel->kernel[-box][x]) + PS_SQR(kernel->kernel[box][x]);
+        }
+        for (int y = -box + 1; y <= box - 1; y++) {
+            // Note: not doing corners
+            sumBox += PS_SQR(kernel->kernel[y][-box]) + PS_SQR(kernel->kernel[y][box]);
+        }
+    }
+
+    psFree(kernel);
+
+    return box;
+}
+
+psImage *p_pmSubtractionPolynomialFromCoords(psImage *output, const pmSubtractionKernels *kernels,
+                                             int numCols, int numRows, int x, int y)
+{
+    assert(kernels);
+    assert(numCols > 0 && numRows > 0);
+
+    // Size to use when calculating normalised coordinates (different from actual size when convolving
+    // subimage)
+    int xNormSize = (kernels->numCols > 0 ? kernels->numCols : numCols);
+    int yNormSize = (kernels->numRows > 0 ? kernels->numRows : numRows);
+
+    // Normalised coordinates
+    float yNorm = 2.0 * (float)(y - yNormSize/2.0) / (float)yNormSize;
+    float xNorm = 2.0 * (float)(x - xNormSize/2.0) / (float)xNormSize;
+
+    return p_pmSubtractionPolynomial(output, kernels->spatialOrder, xNorm, yNorm);
+}
+
+psImage *p_pmSubtractionPolynomial(psImage *output, int spatialOrder, float x, float y)
+{
+    assert(spatialOrder >= 0);
+    assert(x >= -1 && x <= 1);
+    assert(y >= -1 && y <= 1);
+
+    output = psImageRecycle(output, spatialOrder + 1, spatialOrder + 1, PS_TYPE_F64);
+    output->data.F64[0][0] = 1.0;
+
+    double value = 1.0;
+    for (int i = 1; i <= spatialOrder; i++) {
+        value *= x;
+        output->data.F64[0][i] = value;
+    }
+
+    value = 1.0;
+    for (int j = 1; j <= spatialOrder; j++) {
+        value *= y;
+        output->data.F64[j][0] = value;
+    }
+
+    for (int j = 1; j <= spatialOrder; j++) {
+        for (int i = 1; i <= spatialOrder - j; i++) {
+            output->data.F64[j][i] = output->data.F64[j][0] * output->data.F64[0][i];
+        }
+    }
+
+    return output;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+// Convolve a stamp by a single kernel basis function
+static psKernel *convolveStampSingle(const pmSubtractionKernels *kernels, // Kernel basis functions
+                                     int index, // Kernel basis function index
+                                     const psKernel *image, // Image to convolve (a kernel for convenience)
+                                     int footprint // Size of region of interest
+    )
+{
+    switch (kernels->type) {
+      case PM_SUBTRACTION_KERNEL_POIS: {
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+        // Method for SPAM and FRIES is the same
+      case PM_SUBTRACTION_KERNEL_SPAM:
+      case PM_SUBTRACTION_KERNEL_FRIES: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint,
+                                              -footprint, footprint); // Convolved image
+          int uStart = kernels->u->data.S32[index];
+          int uStop = kernels->uStop->data.S32[index];
+          int vStart = kernels->v->data.S32[index];
+          int vStop = kernels->vStop->data.S32[index];
+          float norm = 1.0 / (uStop - uStart + 1) * (vStop - vStart + 1); // Normalisation
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;
+                  for (int v = vStart; v <= vStop; v++) {
+                      for (int u = uStart; u <= uStop; u++) {
+                          sum += image->kernel[y - v][x - u];
+                      }
+                  }
+                  convolved->kernel[y][x] = norm * sum;
+              }
+          }
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_GUNK: {
+          if (index < kernels->inner) {
+              // Photometric scaling is already built in to the precalculated kernel
+              return p_pmSubtractionConvolveStampPrecalc(image, kernels->preCalc->data[index]);
+          }
+          // Using delta function
+          int u = kernels->u->data.S32[index]; // Offset in x
+          int v = kernels->v->data.S32[index]; // Offset in y
+          psKernel *convolved = convolveOffset(image, u, v, footprint); // Convolved image
+          convolveSub(convolved, image, footprint);
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_ISIS: {
+          psArray *preCalc = kernels->preCalc->data[index]; // Precalculated values
+          psVector *xKernel = preCalc->data[0]; // Kernel in x
+          psVector *yKernel = preCalc->data[1]; // Kernel in y
+          int size = kernels->size;     // Size of kernel
+
+          // Convolve in x
+          // Need to convolve a bit more than the footprint, for the y convolution
+          int yMin = -size - footprint, yMax = size + footprint; // Range for y
+          psKernel *temp = psKernelAlloc(yMin, yMax,
+                                         -footprint, footprint); // Temporary convolution; NOTE: wrong way!
+          for (int y = yMin; y <= yMax; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  float value = 0.0;    // Value of convolved pixel
+                  int uMin = x - size, uMax = x + size; // Range for u
+                  psF32 *xKernelData = xKernel->data.F32; // Kernel values
+                  psF32 *imageData = &image->kernel[y][uMin]; // Image values
+                  for (int u = uMin; u <= uMax; u++, xKernelData++, imageData++) {
+                      value += *xKernelData * *imageData;
+                  }
+                  temp->kernel[x][y] = value; // NOTE: putting in wrong way!
+              }
+          }
+
+          // Convolve in y
+          psKernel *convolved = psKernelAlloc(-footprint, footprint, -footprint, footprint);// Convolved image
+          for (int x = -footprint; x <= footprint; x++) {
+              for (int y = -footprint; y <= footprint; y++) {
+                  float value = 0.0;    // Value of convolved pixel
+                  int vMin = y - size, vMax = y + size; // Range for v
+                  psF32 *yKernelData = yKernel->data.F32; // Kernel values
+                  psF32 *imageData = &temp->kernel[x][vMin]; // Image values; NOTE: wrong way!
+                  for (int v = vMin; v <= vMax; v++, yKernelData++, imageData++) {
+                      value += *yKernelData * *imageData;
+                  }
+                  convolved->kernel[y][x] = value;
+              }
+          }
+          psFree(temp);
+
+          // Photometric scaling for even kernels only
+          if (kernels->u->data.S32[index] % 2 == 0 && kernels->v->data.S32[index] % 2 == 0) {
+              convolveSub(convolved, image, footprint);
+          }
+          return convolved;
+      }
+      case PM_SUBTRACTION_KERNEL_RINGS: {
+          psKernel *convolved = psKernelAlloc(-footprint, footprint,
+                                              -footprint, footprint); // Convolved image
+          psArray *preCalc = kernels->preCalc->data[index]; // Precalculated data
+          psVector *uCoords = preCalc->data[0]; // u coordinates
+          psVector *vCoords = preCalc->data[1]; // v coordinates
+          psVector *poly = preCalc->data[2]; // Polynomial values
+          int num = uCoords->n;         // Number of pixels
+          psS32 *uData = uCoords->data.S32, *vData = vCoords->data.S32; // Dereference u,v coordinates
+          psF32 *polyData = poly->data.F32; // Dereference polynomial values
+          psF32 **imageData = image->kernel;  // Dereference image
+          psF32 **convData = convolved->kernel; // Dereference convolved image
+          for (int y = -footprint; y <= footprint; y++) {
+              for (int x = -footprint; x <= footprint; x++) {
+                  double sum = 0.0;             // Accumulated sum from convolution
+                  for (int j = 0; j < num; j++) {
+                      int u = uData[j], v = vData[j]; // Kernel coordinates
+                      sum += imageData[y - v][x - u] * polyData[j];
+                  }
+                  convData[y][x] = sum;
+                  // Photometric scaling is built into the kernel --- no subtraction!
+              }
+          }
+          return convolved;
+      }
+      default:
+        psAbort("Should never get here.");
+    }
+    return NULL;
+}
+
+// Convolve the stamp by each of the kernel basis functions
+static psArray *convolveStamp(psArray *convolutions, // The convolutions
+                              const psKernel *image, // Image to convolve
+                              const pmSubtractionKernels *kernels, // Kernel basis functions
+                              int footprint // Stamp half-size
+    )
+{
+    assert(image);
+    assert(kernels);
+    assert(footprint >= 0);
+
+    if (convolutions) {
+        // Already done
+        return convolutions;
+    }
+
+    int numKernels = kernels->num;      // Number of kernels
+    convolutions = psArrayAlloc(numKernels);
+
+    for (int i = 0; i < numKernels; i++) {
+        convolutions->data[i] = convolveStampSingle(kernels, i, image, footprint);
+    }
+
+    return convolutions;
+}
+
+
+bool pmSubtractionConvolveStamp(pmSubtractionStamp *stamp, const pmSubtractionKernels *kernels, int footprint)
+{
+    PS_ASSERT_PTR_NON_NULL(stamp, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+
+    if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Stamp not marked for calculation.");
+        return false;
+    }
+
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
+        break;
+      case PM_SUBTRACTION_MODE_UNSURE:
+      case PM_SUBTRACTION_MODE_DUAL:
+        stamp->convolutions1 = convolveStamp(stamp->convolutions1, stamp->image1, kernels, footprint);
+        stamp->convolutions2 = convolveStamp(stamp->convolutions2, stamp->image2, kernels, footprint);
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", kernels->mode);
+    }
+
+    return true;
+}
+
+
+
+
+int pmSubtractionRejectStamps(pmSubtractionKernels *kernels, pmSubtractionStampList *stamps,
+                              const psVector *deviations, psImage *subMask, float sigmaRej, int footprint)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, -1);
+    PS_ASSERT_VECTOR_NON_NULL(deviations, -1);
+    PS_ASSERT_VECTOR_TYPE(deviations, PS_TYPE_F32, -1);
+    PS_ASSERT_IMAGE_NON_EMPTY(subMask, -1);
+    PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, -1);
+
+    // I used to measure the rms deviation about zero, and use that as the sigma against which to clip, but
+    // the distribution is actually something like a chi^2 or Student's t, both of which become Gaussian-like
+    // with large N.  Therefore, let's just treat this as a Gaussian distribution.
+
+    kernels->mean = NAN;
+    kernels->rms = NAN;
+    kernels->numStamps = -1;
+
+    int numStamps = 0;                  // Number of used stamps
+    psVector *mask = psVectorAlloc(stamps->num, PS_TYPE_VECTOR_MASK); // Mask, for statistics
+    psVectorInit(mask, 0);
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+            continue;
+        }
+        numStamps++;
+    }
+    psTrace("psModules.imcombine", 1, "Number of good stamps: %d\n", numStamps);
+
+    if (numStamps == 0) {
+        psError(PS_ERR_UNKNOWN, true, "No good stamps found.");
+        psFree(mask);
+        return -1;
+    }
+
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV |
+                                  PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_QUARTILE); // Statistics for deviatns
+    if (!psVectorStats(stats, deviations, NULL, mask, 0xff)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to measure statistics for deviations.");
+        psFree(stats);
+        psFree(mask);
+        return -1;
+    }
+    psFree(mask);
+
+    double mean, rms;                 // Mean and RMS of deviations
+    if (numStamps < MIN_SAMPLE_STATS) {
+        mean = stats->sampleMean;
+        rms = stats->sampleStdev;
+    } else {
+        mean = stats->sampleMedian;
+        rms = 0.74 * (stats->sampleUQ - stats->sampleLQ);
+    }
+    psFree(stats);
+
+    psTrace("psModules.imcombine", 1, "Mean: %f\n", mean);
+    psTrace("psModules.imcombine", 1, "RMS deviation: %f\n", rms);
+
+    kernels->mean = mean;
+    kernels->rms = rms;
+    kernels->numStamps = numStamps;
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Mean deviation from %d stamps: %lf +/- %lf",
+             numStamps, mean, rms);
+
+    if (!isfinite(sigmaRej) || sigmaRej <= 0.0) {
+        // User just wanted to calculate and record the deviation for posterity
+        return 0;
+    }
+
+    float limit = sigmaRej * rms; // Limit on maximum deviation
+    psTrace("psModules.imcombine", 1, "Deviation limit: %f\n", limit);
+
+    int numRejected = 0;                // Number of stamps rejected
+    int numGood = 0;                    // Number of good stamps
+    double newMean = 0.0;               // New mean
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+            // Should we reject stars with low deviation?  Well, if this is really a Gaussian-like
+            // distribution and they're low, then we have the right to ask why.  Isn't it suspicious that
+            // they're anomalously low, compared to the rest of the population which (we hope) is indicative
+            // of normality?  Besides, the standard deviation is going to be blown up by stars that didn't
+            // subtract well, in which case very few (if any) stars will be legitimately rejected for being
+            // low.
+            if (fabsf(deviations->data.F32[i] - mean) > limit) {
+                // Mask out the stamp in the image so you it's not found again
+                psTrace("psModules.imcombine", 3, "Rejecting stamp %d (%d,%d)\n", i,
+                        (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
+                numRejected++;
+                for (int y = stamp->y - footprint; y <= stamp->y + footprint; y++) {
+                    for (int x = stamp->x - footprint; x <= stamp->x + footprint; x++) {
+                        subMask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= PM_SUBTRACTION_MASK_REJ;
+                    }
+                }
+
+                // Set stamp for replacement
+                stamp->x = 0;
+                stamp->y = 0;
+                stamp->xNorm = NAN;
+                stamp->yNorm = NAN;
+                stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+                // Recalculate convolutions
+                psFree(stamp->convolutions1);
+                psFree(stamp->convolutions2);
+                stamp->convolutions1 = stamp->convolutions2 = NULL;
+                psFree(stamp->image1);
+                psFree(stamp->image2);
+                psFree(stamp->variance);
+                stamp->image1 = stamp->image2 = stamp->variance = NULL;
+                psFree(stamp->matrix1);
+                psFree(stamp->matrix2);
+                psFree(stamp->matrixX);
+                stamp->matrix1 = stamp->matrix2 = stamp->matrixX = NULL;
+                psFree(stamp->vector1);
+                psFree(stamp->vector2);
+                stamp->vector1 = stamp->vector2 = NULL;
+            } else {
+                numGood++;
+                newMean += deviations->data.F32[i];
+            }
+        }
+    }
+    newMean /= numGood;
+
+    if (numRejected > 0) {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO,
+                 "%d good stamps; %d rejected.\nMean deviation: %lf --> %lf\n",
+                 numGood, numRejected, mean, newMean);
+    } else {
+        psLogMsg("psModules.imcombine", PS_LOG_INFO,
+                 "%d good stamps; 0 rejected.\nMean deviation: %lf\n",
+                 numGood, mean);
+    }
+
+    return numRejected;
+}
+
+psKernel *pmSubtractionKernel(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y); // Solved polynomial
+    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, wantDual); // The appropriate kernel
+    psFree(polyValues);
+
+    return kernel;
+}
+
+psImage *pmSubtractionKernelImage(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psKernel *kernel = pmSubtractionKernel(kernels, x, y, wantDual); // Convolution kernel
+    psImage *image = psMemIncrRefCounter(kernel->image); // Image of the kernel
+    psFree(kernel);
+
+    return image;
+}
+
+
+float pmSubtractionVarianceFactor(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NAN);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NAN);
+
+    // Precalulate polynomial values
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, kernels->spatialOrder, x, y);
+
+    psKernel *kernel = solvedKernel(NULL, kernels, polyValues, wantDual); // The appropriate kernel
+    psFree(polyValues);
+
+    double sumKernel2 = 0.0;            // Sum of the kernel squared
+    double sumKernel = 0.0;             // Sum of the kernel
+    for (int y = kernel->yMin; y <= kernel->yMax; y++) {
+        for (int x = kernel->xMin; x <= kernel->xMax; x++) {
+            sumKernel += kernel->kernel[y][x];
+            sumKernel2 += PS_SQR(kernel->kernel[y][x]);
+        }
+    }
+
+    psFree(kernel);
+
+    return sumKernel2 / PS_SQR(sumKernel);
+}
+
+#if 0
+psArray *pmSubtractionKernelSolutions(const pmSubtractionKernels *kernels, float x, float y, bool wantDual)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(x, -1.0, 1.0, NULL);
+    PS_ASSERT_FLOAT_WITHIN_RANGE(y, -1.0, 1.0, NULL);
+
+    psArray *images = psArrayAlloc(solution->n - 1); // Images of each kernel to return
+    psVector *fakeSolution = psVectorAlloc(solution->n, PS_TYPE_F64); // Fake solution vector
+    psVectorInit(fakeSolution, 0.0);
+
+    for (int i = 0; i < solution->n - 1; i++) {
+        fakeSolution->data.F64[i] = solution->data.F64[i];
+        images->data[i] = pmSubtractionKernelImage(kernels, x, y, wantDual);
+        fakeSolution->data.F64[i] = 0.0;
+    }
+
+    psFree(fakeSolution);
+
+    return images;
+}
+#endif
+
+
+// XXX Put kernelImage, kernelVariance and polyValues on thread-dependent data
+static bool subtractionConvolvePatch(int numCols, int numRows, // Size of image
+                                     int x0, int y0, // Offsets for image
+                                     pmReadout *out1, pmReadout *out2, // Output readouts
+                                     psImage *convMask, // Output convolved mask
+                                     const pmReadout *ro1, const pmReadout *ro2, // Input readouts
+                                     psImage *sys1, psImage *sys2, // Systematic error images
+                                     psImage *subMask, // Input subtraction mask
+                                     psImageMaskType maskBad, // Mask value to give bad pixels
+                                     psImageMaskType maskPoor, // Mask value to give poor pixels
+                                     float poorFrac, // Fraction for "poor"
+                                     const psRegion *region, // Patch to convolve
+                                     const pmSubtractionKernels *kernels, // Kernels
+                                     bool doBG, // Add in background when convolving?
+                                     bool useFFT // Use FFT to do the convolution?
+    )
+{
+    int size = kernels->size;           // Half-size of kernel
+    int xMin = region->x0, xMax = region->x1, yMin = region->y0, yMax = region->y1; // Bounds of patch
+
+    psKernel *kernelImage = NULL;       // Kernel for the images
+    psKernel *kernelVariance = NULL;      // Kernel for the variance maps
+
+    // Only generate polynomial values every kernel footprint, since we have already assumed
+    // (with the stamps) that it does not vary rapidly on this scale.
+    psImage *polyValues = p_pmSubtractionPolynomialFromCoords(NULL, kernels, numCols, numRows,
+                                                              xMin + x0 + size + 1,
+                                                              yMin + y0 + size + 1);
+    float background = doBG ? p_pmSubtractionSolutionBackground(kernels, polyValues) : 0.0; // Background term
+
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        convolveRegion(out1->image, out1->variance, convMask, &kernelImage, &kernelVariance,
+                       ro1->image, ro1->variance, sys1, subMask, kernels, polyValues, background, *region,
+                       maskBad, maskPoor, poorFrac, useFFT, false);
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        convolveRegion(out2->image, out2->variance, convMask, &kernelImage, &kernelVariance,
+                       ro2->image, ro2->variance, sys2, subMask, kernels, polyValues, background, *region,
+                       maskBad, maskPoor, poorFrac, useFFT, kernels->mode == PM_SUBTRACTION_MODE_DUAL);
+    }
+
+    psFree(kernelImage);
+    psFree(kernelVariance);
+    psFree(polyValues);
+
+    if ((kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro1->mask) {
+        psImageMaskType **target = convMask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
+        psImageMaskType **source = ro1->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask
+
+        for (int y = yMin; y < yMax; y++) {
+            for (int x = xMin; x < xMax; x++) {
+                target[y][x] |= source[y][x];
+            }
+        }
+    }
+    if ((kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) && ro2->mask) {
+        psImageMaskType **target = convMask->data.PS_TYPE_IMAGE_MASK_DATA; // Target mask
+        psImageMaskType **source = ro2->mask->data.PS_TYPE_IMAGE_MASK_DATA; // Source mask
+
+        for (int y = yMin; y < yMax; y++) {
+            for (int x = xMin; x < xMax; x++) {
+                target[y][x] |= source[y][x];
+            }
+        }
+    }
+
+    return true;
+}
+
+bool pmSubtractionConvolveThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psArray *args = job->args;          // Arguments
+    int numCols = PS_SCALAR_VALUE(args->data[0], S32); // Number of columns
+    int numRows = PS_SCALAR_VALUE(args->data[1], S32); // Number of rows
+    int x0 = PS_SCALAR_VALUE(args->data[2], S32); // Offset in x
+    int y0 = PS_SCALAR_VALUE(args->data[3], S32); // Offset in x
+    pmReadout *out1 = args->data[4];    // Output readout 1
+    pmReadout *out2 = args->data[5];    // Output readout 2
+    psImage *convMask = args->data[6];  // Output convolved mask
+    const pmReadout *ro1 = args->data[7]; // Input readout 1
+    const pmReadout *ro2 = args->data[8]; // Input readout 2
+    psImage *sys1 = args->data[9]; // Systematic error image 1
+    psImage *sys2 = args->data[10]; // Systematic error image 2
+    psImage *subMask = args->data[11]; // Subtraction mask
+    psImageMaskType maskBad = PS_SCALAR_VALUE(args->data[12], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for bad pixels
+    psImageMaskType maskPoor = PS_SCALAR_VALUE(args->data[13], PS_TYPE_IMAGE_MASK_DATA); // Output mask value for poor pixels
+    float poorFrac = PS_SCALAR_VALUE(args->data[14], F32); // Fraction for "poor"
+    const psRegion *region = args->data[15]; // Region to convolve
+    const pmSubtractionKernels *kernels = args->data[16]; // Kernels
+    bool doBG = PS_SCALAR_VALUE(args->data[17], U8); // Do background subtraction?
+    bool useFFT = PS_SCALAR_VALUE(args->data[18], U8); // Use FFT for convolution?
+
+    return subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2, sys1, sys2,
+                                    subMask, maskBad, maskPoor, poorFrac, region, kernels, doBG, useFFT);
+}
+
+bool pmSubtractionConvolve(pmReadout *out1, pmReadout *out2, const pmReadout *ro1, const pmReadout *ro2,
+                           psImage *subMask, int stride, psImageMaskType maskBad, psImageMaskType maskPoor,
+                           float poorFrac, float sysError, const psRegion *region,
+                           const pmSubtractionKernels *kernels, bool doBG, bool useFFT)
+{
+    int numCols = 0, numRows = 0;       // Image dimensions
+    int x0 = 0, y0 = 0;                 // Image offset
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PM_ASSERT_READOUT_NON_NULL(out1, false);
+        PM_ASSERT_READOUT_NON_NULL(ro1, false);
+        PM_ASSERT_READOUT_IMAGE(ro1, false);
+        numCols = ro1->image->numCols;
+        numRows = ro1->image->numRows;
+        x0 = ro1->col0;
+        y0 = ro1->row0;
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PM_ASSERT_READOUT_NON_NULL(out2, false);
+        PM_ASSERT_READOUT_NON_NULL(ro2, false);
+        PM_ASSERT_READOUT_IMAGE(ro2, false);
+        if (numCols == 0 && numRows == 0) {
+            numCols = ro2->image->numCols;
+            numRows = ro2->image->numRows;
+            x0 = ro2->col0;
+            y0 = ro2->row0;
+        }
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, false);
+    }
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, false);
+    if (subMask) {
+        PS_ASSERT_IMAGE_NON_NULL(subMask, false);
+        PS_ASSERT_IMAGE_TYPE(subMask, PS_TYPE_IMAGE_MASK, false);
+        PS_ASSERT_IMAGE_SIZE(subMask, numCols, numRows, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(stride, false);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(poorFrac, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(poorFrac, 1.0, false);
+    PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(sysError, 0.0, false);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(sysError, 1.0, false);
+    if (region && psRegionIsNaN(*region)) {
+        psString string = psRegionToString(*region);
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Input region (%s) contains NAN values", string);
+        psFree(string);
+        return false;
+    }
+
+    psTimerStart("pmSubtractionConvolve");
+
+    bool threaded = pmSubtractionThreaded(); // Running threaded?
+
+    // Outputs
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        if (!out1->image) {
+            out1->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            // XXX if (threaded) {
+            // XXX     psMutexInit(out1->image);
+            // XXX }
+        }
+        if (ro1->variance) {
+            if (!out1->variance) {
+                out1->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+                // XXX if (threaded) {
+                // XXX     psMutexInit(out1->variance);
+                // XXX }
+            }
+            psImageInit(out1->variance, 0.0);
+        }
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        if (!out2->image) {
+            out2->image = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+            // XXX if (threaded) {
+            // XXX     psMutexInit(out2->image);
+            // XXX }
+        }
+        if (ro2->variance) {
+            if (!out2->variance) {
+                out2->variance = psImageAlloc(numCols, numRows, PS_TYPE_F32);
+                // XXX if (threaded) {
+                // XXX     psMutexInit(out2->variance);
+                // XXX }
+            }
+            psImageInit(out2->variance, 0.0);
+        }
+    }
+    psImage *convMask = NULL;           // Convolved mask image (common to inputs 1 and 2)
+    if (subMask) {
+        // XXX if (threaded) {
+        // XXX     psMutexInit(subMask);
+        // XXX }
+        if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            if (!out1->mask) {
+                out1->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+            }
+            convMask = out1->mask;
+        }
+        if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            if (convMask) {
+                if (out2->mask) {
+                    psFree(out2->mask);
+                }
+                out2->mask = psMemIncrRefCounter(convMask);
+            } else {
+                if (!out2->mask) {
+                    out2->mask = psImageAlloc(numCols, numRows, PS_TYPE_IMAGE_MASK);
+                }
+                convMask = out2->mask;
+            }
+        }
+        psImageInit(convMask, 0);
+    }
+
+    psImage *sys1 = NULL, *sys2 = NULL; // Systematic error images
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        sys1 = subtractionSysErrImage(ro1->image, sysError);
+        // XXX if (threaded && sys1) {
+        // XXX     psMutexInit(sys1);
+        // XXX }
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        sys2 = subtractionSysErrImage(ro2->image, sysError);
+        // XXX if (threaded && sys2) {
+        // XXX     psMutexInit(sys2);
+        // XXX }
+    }
+
+    int size = kernels->size;           // Half-size of kernel
+
+    // Get region for convolution: [xMin:xMax,yMin:yMax]
+    int xMin = size, xMax = numCols - size;
+    int yMin = size, yMax = numRows - size;
+    if (region) {
+        xMin = PS_MAX(region->x0, xMin);
+        xMax = PS_MIN(region->x1, xMax);
+        yMin = PS_MAX(region->y0, yMin);
+        yMax = PS_MIN(region->y1, yMax);
+    }
+
+#if 0
+    // XXX Use thread-specific data to store these
+    psImage *polyValues = NULL;         // Pre-calculated polynomial values
+    psKernel *kernelImage = NULL;       // Kernel for the images
+    psKernel *kernelVariance = NULL;      // Kernel for the variance maps
+#endif
+
+    // Need to turn off threads at the psLib level --- otherwise, we end up with threads on top of threads,
+    // and everything is executing psThreadPoolWait, waiting for some other mythical thread to complete the
+    // thread's work.
+    bool oldThreads = psImageConvolveSetThreads(false); // Old value of threading for psImageConvolve
+
+    if (stride == 0) {
+        // Use the full size of the kernel
+        stride = 2 * size + 1;
+    }
+
+    for (int j = yMin; j < yMax; j += stride) {
+        int ySubMax = PS_MIN(j + stride, yMax); // Range for subregion of interest
+        for (int i = xMin; i < xMax; i += stride) {
+            int xSubMax = PS_MIN(i + stride, xMax); // Range for subregion of interest
+
+            psRegion *subRegion = psRegionAlloc(i, xSubMax, j, ySubMax); // Bounds of subtraction
+            if (threaded) {
+                psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CONVOLVE");
+                psArray *args = job->args;
+                PS_ARRAY_ADD_SCALAR(args, numCols, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, numRows, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, x0, PS_TYPE_S32);
+                PS_ARRAY_ADD_SCALAR(args, y0, PS_TYPE_S32);
+                psArrayAdd(args, 1, out1);
+                psArrayAdd(args, 1, out2);
+                psArrayAdd(args, 1, convMask);
+                psArrayAdd(args, 1, (pmReadout*)ro1); // Casting away const
+                psArrayAdd(args, 1, (pmReadout*)ro2); // Casting away const
+                // Since adding to the array can impact the reference count, we need to lock
+                // XXX if (sys1) {
+                // XXX     psMutexLock(sys1);
+                // XXX }
+                psArrayAdd(args, 1, sys1);
+                // XXX if (sys1) {
+                // XXX     psMutexUnlock(sys1);
+                // XXX }
+                // XXX if (sys2) {
+                // XXX     psMutexLock(sys2);
+                // XXX }
+                psArrayAdd(args, 1, sys2);
+                // XXX if (sys2) {
+                // XXX     psMutexUnlock(sys2);
+                // XXX }
+                // XXX if (subMask) {
+                // XXX     psMutexLock(subMask);
+                // XXX }
+                psArrayAdd(args, 1, subMask);
+                // XXX if (subMask) {
+                // XXX     psMutexUnlock(subMask);
+                // XXX }
+                PS_ARRAY_ADD_SCALAR(args, maskBad, PS_TYPE_IMAGE_MASK);
+                PS_ARRAY_ADD_SCALAR(args, maskPoor, PS_TYPE_IMAGE_MASK);
+                PS_ARRAY_ADD_SCALAR(args, poorFrac, PS_TYPE_F32);
+                psArrayAdd(args, 1, subRegion);
+                psArrayAdd(args, 1, (pmSubtractionKernels*)kernels); // Casting away const
+                PS_ARRAY_ADD_SCALAR(args, doBG, PS_TYPE_U8);
+                PS_ARRAY_ADD_SCALAR(args, useFFT, PS_TYPE_U8);
+
+                if (!psThreadJobAddPending(job)) {
+                    psFree(job);
+                    return false;
+                }
+                psFree(job);
+            } else {
+                subtractionConvolvePatch(numCols, numRows, x0, y0, out1, out2, convMask, ro1, ro2, sys1, sys2,
+                                         subMask, maskBad, maskPoor, poorFrac, subRegion, kernels, doBG,
+                                         useFFT);
+            }
+            psFree(subRegion);
+        }
+    }
+
+    if (!psThreadPoolWait(false)) {
+        psError(PS_ERR_UNKNOWN, false, "Error waiting for threads.");
+        return false;
+    }
+
+    // We don't rely on psThreadPoolWait to harvest the jobs because the job contains a reference to the
+    // subMask, which is being changed on a thread, and psThreadPoolWait doesn't know that it needs to be
+    // locked before freeing.  After psThreadPoolWait, however, the jobs are completed, the threads are idle,
+    // and so there's no need to lock the subMask when we're blowing away the jobs.
+    if (threaded) {
+        psThreadJob *job;               // Completed job
+        while ((job = psThreadJobGetDone())) {
+            psAssert(strcmp(job->type, "PSMODULES_SUBTRACTION_CONVOLVE") == 0,
+                     "Job has incorrect type: %s", job->type);
+            psFree(job);
+        }
+
+        // XXX if (subMask) {
+        // XXX     psMutexDestroy(subMask);
+        // XXX }
+        // XXX if (sys1) {
+        // XXX     psMutexDestroy(sys1);
+        // XXX }
+        // XXX if (sys2) {
+        // XXX     psMutexDestroy(sys2);
+        // XXX }
+    }
+    psImageConvolveSetThreads(oldThreads);
+
+    psFree(sys1);
+    psFree(sys2);
+
+    // Calculate covariances
+    // This can take a while, so we only do it for a single instance
+    // XXX psImageCovarianceCalculate could be multithreaded
+    if (kernels->mode == PM_SUBTRACTION_MODE_1 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        psKernel *kernel = pmSubtractionKernel(kernels, 0.0, 0.0, false); // Convolution kernel
+        out1->covariance = psImageCovarianceCalculate(kernel, ro1->covariance);
+        psFree(kernel);
+    }
+    if (kernels->mode == PM_SUBTRACTION_MODE_2 || kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+        psKernel *kernel = pmSubtractionKernel(kernels, 0.0, 0.0,
+                                               kernels->mode == PM_SUBTRACTION_MODE_DUAL); // Conv. kernel
+        out2->covariance = psImageCovarianceCalculate(kernel, ro2->covariance);
+        psFree(kernel);
+    }
+
+    // Copy anything that wasn't convolved
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        if (out2) {
+            out2->image = psMemIncrRefCounter(ro2->image);
+            out2->variance = psMemIncrRefCounter(ro2->variance);
+            out2->mask = psMemIncrRefCounter(ro2->mask);
+            out2->covariance = psMemIncrRefCounter(ro2->covariance);
+        }
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        if (out1) {
+            out1->image = psMemIncrRefCounter(ro1->image);
+            out1->variance = psMemIncrRefCounter(ro1->variance);
+            out1->mask = psMemIncrRefCounter(ro1->mask);
+            out1->covariance = psMemIncrRefCounter(ro1->covariance);
+        }
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        break;
+      default:
+        psAbort("Should never get here.");
+    }
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolve image: %f sec",
+             psTimerClear("pmSubtractionConvolve"));
+
+    return true;
+}
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionAnalysis.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionAnalysis.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionAnalysis.c	(revision 21432)
@@ -0,0 +1,275 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <pslib.h>
+
+#include "pmFPA.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionEquation.h"
+
+#include "pmSubtractionAnalysis.h"
+#include "pmSubtractionVisual.h"
+
+#define KERNEL_MOSAIC 2                 // Half-number of kernel instances in the mosaic image
+
+
+bool pmSubtractionAnalysis(psMetadata *analysis, pmSubtractionKernels *kernels, psRegion *region,
+                           int numCols, int numRows)
+{
+    if (analysis) {
+        PS_ASSERT_METADATA_NON_NULL(analysis, false);
+    }
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, false);
+
+    // Record region for subtraction
+    {
+        psRegion *subRegion;    // Region over which subtraction was performed
+        if (region) {
+            subRegion = psMemIncrRefCounter(region);
+        } else {
+            subRegion = psRegionAlloc(0, numCols, 0, numRows);
+        }
+
+        psMetadataAddPtr(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_REGION,
+                         PS_DATA_REGION | PS_META_DUPLICATE_OK,
+                         "Region over which subtraction was performed", subRegion);
+        psFree(subRegion);
+    }
+
+    // Record kernel
+    psMetadataAddPtr(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_KERNEL,
+                     PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "Subtraction kernels", kernels);
+    psMetadataAddS32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MODE,
+                     PS_META_DUPLICATE_OK, "Subtraction kernels", kernels->mode);
+
+    // Realisations of kernel
+    {
+        psTrace("psModules.imcombine", 2, "Generating diagnostics...\n");
+        // Generate image with convolution kernels
+        int size = kernels->size;       // Half-size of kernel
+        int fullSize = 2 * size + 1 + 1; // Full size of kernel
+        int imageSize = (2 * KERNEL_MOSAIC + 1) * fullSize;
+        psImage *convKernels = psImageAlloc((kernels->mode == PM_SUBTRACTION_MODE_DUAL ? 2 : 1) *
+                                            imageSize - 1 +
+                                            (kernels->mode == PM_SUBTRACTION_MODE_DUAL ? 4 : 0),
+                                            imageSize - 1, PS_TYPE_F32);
+        psImageInit(convKernels, NAN);
+        for (int j = -KERNEL_MOSAIC; j <= KERNEL_MOSAIC; j++) {
+            for (int i = -KERNEL_MOSAIC; i <= KERNEL_MOSAIC; i++) {
+                psImage *kernel = pmSubtractionKernelImage(kernels, (float)i / (float)KERNEL_MOSAIC,
+                                                           (float)j / (float)KERNEL_MOSAIC,
+                                                           false); // Image of the kernel
+                if (!kernel) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to generate kernel image.");
+                    psFree(convKernels);
+                    return false;
+                }
+
+                if (psImageOverlaySection(convKernels, kernel, (i + KERNEL_MOSAIC) * fullSize,
+                                          (j + KERNEL_MOSAIC) * fullSize, "=") == 0) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to overlay kernel image.");
+                    psFree(kernel);
+                    psFree(convKernels);
+                    return false;
+                }
+                psFree(kernel);
+
+                if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+                    kernel = pmSubtractionKernelImage(kernels, (float)i / (float)KERNEL_MOSAIC,
+                                                      (float)j / (float)KERNEL_MOSAIC,
+                                                      true); // Image of the kernel
+                    if (!kernel) {
+                        psError(PS_ERR_UNKNOWN, false, "Unable to generate kernel image.");
+                        psFree(convKernels);
+                        return false;
+                    }
+
+                    if (psImageOverlaySection(convKernels, kernel,
+                                              (2 * KERNEL_MOSAIC + 1 + i + KERNEL_MOSAIC) * fullSize + 4,
+                                              (j + KERNEL_MOSAIC) * fullSize, "=") == 0) {
+                        psError(PS_ERR_UNKNOWN, false, "Unable to overlay kernel image.");
+                        psFree(kernel);
+                        psFree(convKernels);
+                        return false;
+                    }
+                    psFree(kernel);
+                }
+            }
+        }
+
+        pmSubtractionVisualPlotConvKernels(convKernels);
+        psMetadataAddImage(analysis, PS_LIST_TAIL, "SUBTRACTION.KERNEL.IMAGE",
+                           PS_META_DUPLICATE_OK, "Realisations of kernel", convKernels);
+        psFree(convKernels);
+    }
+
+
+#if 0
+    // Generate images of the kernel components
+    {
+        psMetadata *header = psMetadataAlloc(); // Header
+        for (int i = 0; i < solution->n; i++) {
+            psString name = NULL;       // Header keyword
+            psStringAppend(&name, "SOLN%04d", i);
+            psMetadataAddF64(header, PS_LIST_TAIL, name, 0, NULL, solution->data.F64[i]);
+            psFree(name);
+        }
+        psArray *kernelImages = pmSubtractionKernelSolutions(solution, kernels, 0.0, 0.0);
+        psFits *kernelFile = psFitsOpen("kernels.fits", "w");
+        (void)psFitsWriteImageCube(kernelFile, header, kernelImages, NULL);
+        psFitsClose(kernelFile);
+        psFree(kernelImages);
+        psFree(header);
+    }
+#endif
+
+
+    // Set the variance factors
+    float vf1 = 1.0, vf2 = 1.0;         // Variance factors for each image
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        vf1 = pmSubtractionVarianceFactor(kernels, 0.5, 0.5, false);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        vf2 = pmSubtractionVarianceFactor(kernels, 0.5, 0.5, false);
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        vf1 = pmSubtractionVarianceFactor(kernels, 0.5, 0.5, false);
+        vf2 = pmSubtractionVarianceFactor(kernels, 0.5, 0.5, true);
+        break;
+      default:
+        psAbort("Invalid subtraction mode: %x", kernels->mode);
+    }
+
+    // Weight by the area
+    if (region) {
+        float norm = (region->x1 - region->x0 + 1) * (region->y1 - region->y0 + 1) / (numCols * numRows);
+        vf1 *= norm;
+        vf2 *= norm;
+    }
+
+    // Update the variance factor
+#define UPDATE_VARFACTOR(VF, ANALYSIS) { \
+    psMetadataItem *vfItem = psMetadataLookup(analysis, ANALYSIS); \
+    if (vfItem) { \
+        psAssert(vfItem->type == PS_TYPE_F32, "Should be the type we said."); \
+        vfItem->data.F32 += VF; \
+    } else { \
+        psMetadataAddF32(analysis, PS_LIST_TAIL, ANALYSIS, 0, "Variance factor weighted by the area", VF); \
+    } \
+}
+
+    UPDATE_VARFACTOR(vf1, PM_SUBTRACTION_ANALYSIS_VARFACTOR_1);
+    UPDATE_VARFACTOR(vf2, PM_SUBTRACTION_ANALYSIS_VARFACTOR_2);
+
+    // Kernel shape
+    {
+        psImage *image = pmSubtractionKernelImage(kernels, 0.5, 0.5, false); // Image of the kernel
+        if (!image) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate image of kernel.");
+            return false;
+        }
+        int size = kernels->size;       // Half-size of kernel
+        int fullSize = 2 * size + 1;    // Full size of kernel
+        float norm = 0.0;               // Normalisation (kernel sum)
+        for (int y = 0; y < fullSize; y++) {
+            for (int x = 0; x < fullSize; x++) {
+                norm += image->data.F32[y][x];
+            }
+        }
+        float max = -INFINITY;          // Maximum fraction
+        for (int r = 1; r < size; r++) {
+            unsigned long r2 = PS_SQR(r); // r^2
+            float sum = 0.0;            // Sum within circle
+            for (int y = 0, v = -size; y < fullSize; y++, v++) {
+                unsigned long v2 = PS_SQR(v); // y^2
+                for (int x = 0, u = -size; x < fullSize; x++, u++) {
+                    unsigned long u2 = PS_SQR(u); // u^2
+                    if (u2 + v2 <= r2) {
+                        sum += image->data.F32[y][x];
+                    }
+                }
+            }
+            float frac = sum / norm;    // Fraction of flux moving towards centre
+            psTrace("psModules.imcombine", 5, "Deconvolution fraction at %d: %f\n", r, frac);
+            max = PS_MAX(max, frac);
+        }
+        psFree(image);
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DECONV_MAX,
+                         PS_META_DUPLICATE_OK, "Maximum deconvolution fraction", max);
+    }
+
+    // Kernel moments
+    {
+        psImage *image = pmSubtractionKernelImage(kernels, 0.5, 0.5, false); // Image of the kernel
+        if (!image) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to generate image of kernel.");
+            return false;
+        }
+        double m00 = 0, m10 = 0, m01 = 0, m20 = 0, m11 = 0, m02 = 0; // Moments to calculate
+        int size = kernels->size;       // Half-size of kernel
+        int fullSize = 2 * size + 1;    // Full size of kernel
+        for (int y = 0, v = -size; y < fullSize; y++, v++) {
+            for (int x = 0, u = -size; x < fullSize; x++, u++) {
+                float value = image->data.F32[y][x]; // Value of kernel
+                m00 += value;
+                m10 += u * value;
+                m01 += v * value;
+                m20 += u * u * value;
+                m11 += u * v * value;
+                m02 += v * v * value;
+            }
+        }
+        psFree(image);
+
+        // Convert first moments to centroids
+        m10 /= m00;
+        m01 /= m00;
+
+        // Convert second moments to covariance
+        m20 = m20 / m00 - PS_SQR(m10);
+        m02 = m02 / m00 - PS_SQR(m01);
+        m11 = m11 / m00 - m10 * m01;
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_NORM,
+                         PS_META_DUPLICATE_OK, "Normalisation", m00);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MX,
+                         PS_META_DUPLICATE_OK, "Moment in x", m10);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MY,
+                         PS_META_DUPLICATE_OK, "Moment in y", m01);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXX,
+                         PS_META_DUPLICATE_OK, "Moment in xx", m20);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MXY,
+                         PS_META_DUPLICATE_OK, "Moment in xy", m11);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_MYY,
+                         PS_META_DUPLICATE_OK, "Moment in yy", m02);
+    }
+
+    // Difference in background
+    {
+        psImage *polyValues = p_pmSubtractionPolynomialFromCoords(NULL, kernels, numCols, numRows,
+                                                                  numCols / 2.0, numRows / 2.0); // Polynomial
+        float bg = p_pmSubtractionSolutionBackground(kernels, polyValues); // Background difference
+
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_BGDIFF,
+                         PS_META_DUPLICATE_OK, "Background difference", bg);
+        psFree(polyValues);
+    }
+
+    // Quality of fit
+    {
+        psMetadataAddS32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_STAMPS, 0, "Number of stamps",
+                         kernels->numStamps);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_MEAN, 0, "Mean stamp deviation",
+                         kernels->mean);
+        psMetadataAddF32(analysis, PS_LIST_TAIL, PM_SUBTRACTION_ANALYSIS_DEV_RMS, 0, "RMS stamp deviation",
+                         kernels->rms);
+    }
+
+    return true;
+}
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionEquation.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionEquation.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionEquation.c	(revision 21432)
@@ -0,0 +1,1161 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmSubtraction.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionThreads.h"
+
+#include "pmSubtractionEquation.h"
+#include "pmSubtractionVisual.h"
+
+//#define TESTING
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Private (file-static) functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the sum over a stamp product
+static inline double calculateSumProduct(const psKernel *image1, // First image in multiplication
+                                         const psKernel *image2, // Second image in multiplication
+                                         const psKernel *variance, // Variance image
+                                         int footprint // (Half-)Size of stamp
+    )
+{
+    double sum = 0.0;                   // Sum of the image products
+    for (int y = - footprint; y <= footprint; y++) {
+        for (int x = - footprint; x <= footprint; x++) {
+            sum += image1->kernel[y][x] * image2->kernel[y][x] / 1.0; // variance->kernel[y][x];
+        }
+    }
+    return sum;
+}
+
+// Calculate a single element of the least-squares matrix, with the polynomial expansions in one direction
+static inline bool calculateMatrixElement1(psImage *matrix, // Matrix to calculate
+                                           int i, int j, // Coordinates of element
+                                           const psKernel *image1, // First image in multiplication
+                                           const psKernel *image2, // Second image in multiplication
+                                           const psKernel *variance, // Variance image
+                                           const psImage *polyValues, // Spatial polynomial values
+                                           int numKernels, // Number of kernel basis functions
+                                           int footprint, // (Half-)Size of stamp
+                                           int spatialOrder, // Maximum order of spatial variation
+                                           bool symmetric // Is the matrix symmetric?
+    )
+{
+    double sum = calculateSumProduct(image1, image2, variance, footprint); // Sum of the image products
+    if (!isfinite(sum)) {
+        return false;
+    }
+
+    // Generate the pseudo-convolutions from the spatial polynomial terms
+    for (int iyOrder = 0, iIndex = i; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex += numKernels) {
+            double convPoly = sum * polyValues->data.F64[iyOrder][ixOrder];
+
+            assert(iIndex < matrix->numRows && j < matrix->numCols);
+
+            matrix->data.F64[iIndex][j] = convPoly;
+            if (symmetric) {
+
+                assert(iIndex < matrix->numCols && j < matrix->numRows);
+
+                matrix->data.F64[j][iIndex] = convPoly;
+            }
+        }
+    }
+    return true;
+}
+
+// Calculate a single element of the least-squares matrix, with the polynomial expansions in both directions
+static inline bool calculateMatrixElement2(psImage *matrix, // Matrix to calculate
+                                           int i, int j, // Coordinates of element
+                                           const psKernel *image1, // First image in multiplication
+                                           const psKernel *image2, // Second image in multiplication
+                                           const psKernel *variance, // Variance image
+                                           const psImage *polyValues, // Spatial polynomial values
+                                           int numKernels, // Number of kernel basis functions
+                                           int footprint, // (Half-)Size of stamp
+                                           int spatialOrder, // Maximum order of spatial variation
+                                           bool symmetric // Is the matrix symmetric?
+    )
+{
+    double sum = calculateSumProduct(image1, image2, variance, footprint); // Sum of the image products
+    if (!isfinite(sum)) {
+        return false;
+    }
+
+    // Generate the pseudo-convolutions from the spatial polynomial terms
+    for (int iyOrder = 0, iIndex = i; iyOrder <= spatialOrder; iyOrder++) {
+        for (int ixOrder = 0; ixOrder <= spatialOrder - iyOrder; ixOrder++, iIndex += numKernels) {
+            double iPoly = polyValues->data.F64[iyOrder][ixOrder]; // Value of polynomial
+            for (int jyOrder = 0, jIndex = j; jyOrder <= spatialOrder; jyOrder++) {
+                for (int jxOrder = 0; jxOrder <= spatialOrder - jyOrder; jxOrder++, jIndex += numKernels) {
+                    double convPoly = sum * iPoly * polyValues->data.F64[jyOrder][jxOrder];
+
+                    assert(iIndex < matrix->numRows && jIndex < matrix->numCols);
+
+                    matrix->data.F64[iIndex][jIndex] = convPoly;
+                    if (symmetric) {
+
+                        assert(iIndex < matrix->numCols && jIndex < matrix->numRows);
+
+                        matrix->data.F64[jIndex][iIndex] = convPoly;
+                    }
+                }
+            }
+        }
+    }
+    return true;
+}
+
+// Calculate the square part of the matrix derived from multiplying convolutions
+static bool calculateMatrixSquare(psImage *matrix, // Matrix to calculate
+                                  const psArray *convolutions1, // Convolutions for element 1
+                                  const psArray *convolutions2, // Convolutions for element 2
+                                  const psKernel *variance, // Variance image
+                                  const psImage *polyValues, // Polynomial values
+                                  int numKernels, // Number of kernel basis functions
+                                  int spatialOrder, // Order of spatial variation
+                                  int footprint // Half-size of stamp
+                                  )
+{
+    bool symmetric = (convolutions1 == convolutions2 ? true : false); // Is matrix symmetric?
+
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *iConv = convolutions1->data[i]; // Convolution for i-th element
+
+        for (int j = (symmetric ? i : 0); j < numKernels; j++) {
+            psKernel *jConv = convolutions2->data[j]; // Convolution for j-th element
+
+            if (!calculateMatrixElement2(matrix, i, j, iConv, jConv, variance, polyValues, numKernels,
+                                         footprint, spatialOrder, symmetric)) {
+                psTrace("psModules.imcombine", 2, "Bad sumCC at %d, %d", i, j);
+                return false;
+            }
+        }
+    }
+
+    return true;
+}
+
+// Calculate least-squares matrix and vector
+static bool calculateMatrix(psImage *matrix, // Matrix to calculate
+                            const pmSubtractionKernels *kernels, // Kernel components
+                            const psArray *convolutions, // Convolutions of source with kernels
+                            const psKernel *input, // Input stamp, or NULL
+                            const psKernel *variance, // Variance stamp
+                            const psImage *polyValues, // Spatial polynomial values
+                            int footprint, // (Half-)Size of stamp
+                            bool normAndBG // Calculate normalisation and background terms?
+    )
+{
+    int numKernels = kernels->num;      // Number of kernel components
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variation terms
+    int bgOrder = kernels->bgOrder;     // Maximum order of background fit
+    int numBackground = normAndBG ? PM_SUBTRACTION_POLYTERMS(bgOrder) : 0; // Number of background terms
+    int numTerms = numKernels * numSpatial + (normAndBG ? 1 + numBackground : 0); // Total number of terms
+    assert(matrix);
+    assert(matrix->numCols == matrix->numRows);
+    assert(matrix->numCols == numTerms);
+    assert(convolutions && convolutions->n == numKernels);
+    assert(polyValues);
+    assert(!normAndBG || input);        // If we want the normalisation and BG, then we need the input image
+
+    // Square part of the matrix (convolution-convolution products)
+    if (!calculateMatrixSquare(matrix, convolutions, convolutions, variance, polyValues, numKernels,
+                               spatialOrder, footprint)) {
+        return false;
+    }
+
+    // XXX To support higher-order background model than simply constant, the below code needs to be updated.
+    if (normAndBG) {
+        int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+        int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index in matrix for background
+
+        for (int i = 0; i < numKernels; i++) {
+            psKernel *conv = convolutions->data[i]; // Convolution for i-th element
+
+            // Normalisation-convolution terms
+            if (!calculateMatrixElement1(matrix, i, normIndex, conv, input, variance, polyValues, numKernels,
+                                         footprint, spatialOrder, true)) {
+                psTrace("psModules.imcombine", 2, "Bad sumIC at %d", i);
+                return false;
+            }
+
+            // Background-convolution terms
+            double sumC = 0.0;          // Sum of the convolution
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    sumC += conv->kernel[y][x] / 1.0; // variance->kernel[y][x];
+                }
+            }
+            if (!isfinite(sumC)) {
+                psTrace("psModules.imcombine", 2, "Bad sumC at %d", i);
+                return false;
+            }
+
+            for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+                for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                    double value = sumC * polyValues->data.F64[yOrder][xOrder];
+                    matrix->data.F64[index][bgIndex] = value;
+                    matrix->data.F64[bgIndex][index] = value;
+                }
+            }
+        }
+
+        // Background only, normalisation only, and background-normalisation terms
+        double sum1 = 0.0;              // Sum of the weighting
+        double sumI = 0.0;              // Sum of the input
+        double sumII = 0.0;             // Sum of the input squared
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double invNoise2 = 1.0 / 1.0; // variance->kernel[y][x];
+                double value = input->kernel[y][x] * invNoise2;
+                sumI += value;
+                sumII += value * input->kernel[y][x];
+                sum1 += invNoise2;
+            }
+        }
+        if (!isfinite(sumI)) {
+            psTrace("psModules.imcombine", 2, "Bad sumI detected");
+            return false;
+        }
+        if (!isfinite(sumII)) {
+            psTrace("psModules.imcombine", 2, "Bad sumII detected");
+            return false;
+        }
+        if (!isfinite(sum1)) {
+            psTrace("psModules.imcombine", 2, "Bad sum1 detected");
+            return false;
+        }
+        matrix->data.F64[normIndex][normIndex] = sumII;
+        matrix->data.F64[bgIndex][bgIndex] = sum1;
+        matrix->data.F64[normIndex][bgIndex] = sumI;
+        matrix->data.F64[bgIndex][normIndex] = sumI;
+    }
+
+    return true;
+}
+
+
+// Calculate least-squares matrix and vector
+static bool calculateVector(psVector *vector, // Vector to calculate, or NULL
+                            const pmSubtractionKernels *kernels, // Kernel components
+                            const psArray *convolutions, // Convolutions of source with kernels
+                            const psKernel *input, // Input stamp, or NULL if !normAndBG
+                            const psKernel *target, // Target stamp
+                            const psKernel *variance, // Variance stamp
+                            const psImage *polyValues, // Spatial polynomial values
+                            int footprint, // (Half-)Size of stamp
+                            bool normAndBG // Calculate normalisation and background terms?
+    )
+{
+    int numKernels = kernels->num;      // Number of kernel components
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variation terms
+    int bgOrder = kernels->bgOrder;     // Maximum order of background fit
+    int numBackground = normAndBG ? PM_SUBTRACTION_POLYTERMS(bgOrder) : 0; // Number of background terms
+    int numTerms = numKernels * numSpatial + (normAndBG ? 1 + numBackground : 0); // Total number of terms
+    assert(vector && vector->n == numTerms);
+    assert(convolutions && convolutions->n == numKernels);
+    assert(target);
+    assert(polyValues);
+    assert(!normAndBG || input);       // If we want the normalisation and BG, then we need the input image
+
+    // Convolution terms
+    for (int i = 0; i < numKernels; i++) {
+        psKernel *conv = convolutions->data[i]; // Convolution for i-th element
+        double sumTC = 0.0;          // Sum of the target and convolution
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                sumTC += target->kernel[y][x] * conv->kernel[y][x] / 1.0; // variance->kernel[y][x];
+            }
+        }
+        if (!isfinite(sumTC)) {
+            psTrace("psModules.imcombine", 2, "Bad sumTC at %d", i);
+            return false;
+        }
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                vector->data.F64[index] = sumTC * polyValues->data.F64[yOrder][xOrder];
+            }
+        }
+    }
+
+    if (normAndBG) {
+        // Background terms
+        double sumT = 0.0;              // Sum of the target
+        double sumIT = 0.0;             // Sum of the input-target product
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                float value = target->kernel[y][x] / 1.0; // variance->kernel[y][x];
+                sumIT += value * input->kernel[y][x];
+                sumT += value;
+            }
+        }
+        if (!isfinite(sumT)) {
+            psTrace("psModules.imcombine", 2, "Bad sumI detected");
+            return false;
+        }
+        if (!isfinite(sumIT)) {
+            psTrace("psModules.imcombine", 2, "Bad sumIT detected");
+            return false;
+        }
+
+        int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation term
+        vector->data.F64[normIndex] = sumIT;
+        int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background term
+        vector->data.F64[bgIndex] = sumT;
+    }
+
+    return true;
+}
+
+
+
+// Calculate the cross-matrix, composed of convolutions of each image
+// Note that the cross-matrix is NOT square
+static bool calculateMatrixCross(psImage *matrix, // Matrix to calculate
+                                 const pmSubtractionKernels *kernels, // Kernel components
+                                 const psArray *convolutions1, // Convolutions of image 1
+                                 const psArray *convolutions2, // Convolutions of image 2
+                                 const psKernel *image1, // Image 1 stamp
+                                 const psKernel *variance, // Variance stamp
+                                 const psImage *polyValues, // Spatial polynomial values
+                                 int footprint // (Half-)Size of stamp
+                                 )
+{
+    assert(matrix);
+    int numKernels = kernels->num;      // Number of kernel components
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial polynomial terms
+    int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+    int numCols = numKernels * numSpatial + 1 + numBackground; // Number of columns
+    int numRows = numKernels * numSpatial; // Number of rows
+    assert(matrix->numCols == numCols && matrix->numRows == numRows);
+    assert(convolutions1 && convolutions1->n == numKernels);
+    assert(convolutions2 && convolutions2->n == numKernels);
+
+    int normIndex, bgIndex;             // Indices in matrix for normalisation and background terms
+    PM_SUBTRACTION_INDICES(normIndex, bgIndex, kernels);
+
+    if (!calculateMatrixSquare(matrix, convolutions1, convolutions2, variance, polyValues, numKernels,
+                               spatialOrder, footprint)) {
+        return false;
+    }
+
+    for (int i = 0; i < numKernels; i++) {
+        // Normalisation
+        psKernel *conv = convolutions2->data[i]; // Convolution
+        if (!calculateMatrixElement1(matrix, i, normIndex, conv, image1, variance, polyValues, numKernels,
+                                     footprint, spatialOrder, false)) {
+            psTrace("psModules.imcombine", 2, "Bad sumIC at %d", i);
+            return false;
+        }
+
+        // Background
+        double sumC = 0.0;              // Sum of the weighting
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                sumC += conv->kernel[y][x] / 1.0; // variance->kernel[y][x];
+            }
+        }
+        if (!isfinite(sumC)) {
+            psTrace("psModules.imcombine", 2, "Bad sumC detected at %d", i);
+            return false;
+        }
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                matrix->data.F64[index][bgIndex] = sumC * polyValues->data.F64[yOrder][xOrder];
+            }
+        }
+    }
+
+    return true;
+}
+
+
+// Add in penalty term to least-squares vector
+static bool calculatePenalty(psVector *vector, // Vector to which to add in penalty term
+                             const pmSubtractionKernels *kernels // Kernel parameters
+    )
+{
+    if (kernels->penalty == 0.0) {
+        return true;
+    }
+
+    psVector *penalties = kernels->penalties; // Penalties for each kernel component
+    int spatialOrder = kernels->spatialOrder; // Order of spatial variations
+    int numKernels = kernels->num; // Number of kernel components
+    for (int i = 0; i < numKernels; i++) {
+        for (int yOrder = 0, index = i; yOrder <= spatialOrder; yOrder++) {
+            for (int xOrder = 0; xOrder <= spatialOrder - yOrder; xOrder++, index += numKernels) {
+                vector->data.F64[index] -= penalties->data.F32[i];
+            }
+        }
+    }
+
+    return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Semi-public functions
+// XXX We might like to define these functions as "extern inline" but gcc currently doesn't handle this in c99
+// mode.  See http://gcc.gnu.org/ml/gcc/2006-11/msg00006.html
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+// Calculate the value of a polynomial, specified by coefficients and polynomial values
+double p_pmSubtractionCalculatePolynomial(const psVector *coeff, // Coefficients
+                                                 const psImage *polyValues, // Polynomial values
+                                                 int order, // Order of polynomials
+                                                 int index, // Index at which to begin
+                                                 int step // Step between subsequent indices
+                                                 )
+{
+    double sum = 0.0;                   // Value of the polynomial sum
+    for (int yOrder = 0; yOrder <= order; yOrder++) {
+        for (int xOrder = 0; xOrder <= order - yOrder; xOrder++, index += step) {
+
+            assert(index < coeff->n);
+
+            sum += coeff->data.F64[index] * polyValues->data.F64[yOrder][xOrder];
+        }
+    }
+    return sum;
+}
+
+double p_pmSubtractionSolutionCoeff(const pmSubtractionKernels *kernels, const psImage *polyValues,
+                                           int index, bool wantDual)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+    PS_ASSERT_INT_POSITIVE(index, NAN);
+#endif
+
+    psVector *solution = wantDual ? kernels->solution2 : kernels->solution1; // Solution vector
+    return p_pmSubtractionCalculatePolynomial(solution, polyValues, kernels->spatialOrder, index,
+                                              kernels->num);
+}
+
+double p_pmSubtractionSolutionNorm(const pmSubtractionKernels *kernels)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+#endif
+
+    int normIndex = PM_SUBTRACTION_INDEX_NORM(kernels); // Index for normalisation
+    return kernels->solution1->data.F64[normIndex];
+}
+
+double p_pmSubtractionSolutionBackground(const pmSubtractionKernels *kernels,
+                                                const psImage *polyValues)
+{
+#if 0
+    // This is probably in a tight loop, so don't check inputs
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NAN);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NAN);
+    PS_ASSERT_IMAGE_NON_NULL(polyValues, NAN);
+#endif
+
+    int bgIndex = PM_SUBTRACTION_INDEX_BG(kernels); // Index for background
+    return p_pmSubtractionCalculatePolynomial(kernels->solution1, polyValues, kernels->bgOrder, bgIndex, 1);
+}
+
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Public functions
+//////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+bool pmSubtractionCalculateEquationThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    pmSubtractionStampList *stamps = job->args->data[0]; // List of stamps
+    const pmSubtractionKernels *kernels = job->args->data[1]; // Kernels
+    int index = PS_SCALAR_VALUE(job->args->data[2], S32); // Stamp index
+
+    return pmSubtractionCalculateEquationStamp(stamps, kernels, index);
+}
+
+bool pmSubtractionCalculateEquationStamp(pmSubtractionStampList *stamps, const pmSubtractionKernels *kernels,
+                                         int index)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PS_ASSERT_INT_NONNEGATIVE(index, false);
+    PS_ASSERT_INT_LESS_THAN(index, stamps->num, false);
+
+    int footprint = stamps->footprint;  // Half-size of stamps
+    int spatialOrder = kernels->spatialOrder; // Maximum order of spatial variation
+    int numKernels = kernels->num;      // Number of kernel basis functions
+    int numSpatial = PM_SUBTRACTION_POLYTERMS(spatialOrder); // Number of spatial variations
+    int numBackground = PM_SUBTRACTION_POLYTERMS(kernels->bgOrder); // Number of background terms
+
+    // Total number of parameters to solve for: coefficient of each kernel basis function, multipled by the
+    // number of coefficients for the spatial polynomial, normalisation and a constant background offset.
+    int numParams = numKernels * numSpatial + 1 + numBackground;
+
+    pmSubtractionStamp *stamp = stamps->stamps->data[index]; // Stamp of interest
+    psAssert(stamp->status == PM_SUBTRACTION_STAMP_CALCULATE, "We only operate on stamps with this state.");
+
+    // Generate convolutions
+    if (!pmSubtractionConvolveStamp(stamp, kernels, footprint)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to convolve stamp %d.", index);
+        return NULL;
+    }
+
+#ifdef TESTING
+    for (int j = 0; j < numKernels; j++) {
+        if (stamp->convolutions1) {
+            psString convName = NULL;
+            psStringAppend(&convName, "conv1_%03d_%03d.fits", index, j);
+            psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions1->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+
+        if (stamp->convolutions2) {
+            psString convName = NULL;
+            psStringAppend(&convName, "conv2_%03d_%03d.fits", index, j);
+            psFits *fits = psFitsOpen(convName, "w");
+            psFree(convName);
+            psKernel *conv = stamp->convolutions2->data[j];
+            psFitsWriteImage(fits, NULL, conv->image, 0, NULL);
+            psFitsClose(fits);
+        }
+    }
+#endif
+
+    psImage *polyValues = p_pmSubtractionPolynomial(NULL, spatialOrder,
+                                                    stamp->xNorm, stamp->yNorm); // Polynomial terms
+
+    bool new = stamp->vector1 ? false : true; // Is this a new run?
+    if (new) {
+        stamp->matrix1 = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        stamp->vector1 = psVectorAlloc(numParams, PS_TYPE_F64);
+    }
+#ifdef TESTING
+    psImageInit(stamp->matrix1, NAN);
+    psVectorInit(stamp->vector1, NAN);
+#endif
+
+    bool status;                    // Status of least-squares matrix/vector calculation
+    switch (kernels->mode) {
+      case PM_SUBTRACTION_MODE_1:
+        status = calculateMatrix(stamp->matrix1, kernels, stamp->convolutions1, stamp->image1,
+                                 stamp->variance, polyValues, footprint, true);
+        status &= calculateVector(stamp->vector1, kernels, stamp->convolutions1, stamp->image1,
+                                  stamp->image2, stamp->variance, polyValues, footprint, true);
+        break;
+      case PM_SUBTRACTION_MODE_2:
+        status = calculateMatrix(stamp->matrix1, kernels, stamp->convolutions2, stamp->image2,
+                                 stamp->variance, polyValues, footprint, true);
+        status &= calculateVector(stamp->vector1, kernels, stamp->convolutions2, stamp->image2,
+                                  stamp->image1, stamp->variance, polyValues, footprint, true);
+        break;
+      case PM_SUBTRACTION_MODE_DUAL:
+        if (new) {
+            stamp->matrix2 = psImageAlloc(numKernels * numSpatial, numKernels * numSpatial, PS_TYPE_F64);
+            stamp->matrixX = psImageAlloc(numParams, numKernels * numSpatial, PS_TYPE_F64);
+            stamp->vector2 = psVectorAlloc(numKernels * numSpatial, PS_TYPE_F64);
+        }
+#ifdef TESTING
+        psImageInit(stamp->matrix2, NAN);
+        psImageInit(stamp->matrixX, NAN);
+        psVectorInit(stamp->vector2, NAN);
+#endif
+        status  = calculateMatrix(stamp->matrix1, kernels, stamp->convolutions1, stamp->image1,
+                                  stamp->variance, polyValues, footprint, true);
+        status &= calculateMatrix(stamp->matrix2, kernels, stamp->convolutions2, NULL,
+                                  stamp->variance, polyValues, footprint, false);
+        status &= calculateMatrixCross(stamp->matrixX, kernels, stamp->convolutions1,
+                                       stamp->convolutions2, stamp->image1, stamp->variance, polyValues,
+                                       footprint);
+        status &= calculateVector(stamp->vector1, kernels, stamp->convolutions1, stamp->image1,
+                                  stamp->image2, stamp->variance, polyValues, footprint, true);
+        status &= calculateVector(stamp->vector2, kernels, stamp->convolutions2, NULL,
+                                  stamp->image2, stamp->variance, polyValues, footprint, false);
+        break;
+      default:
+        psAbort("Unsupported subtraction mode: %x", kernels->mode);
+    }
+
+    if (!status) {
+        stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+        psWarning("Rejecting stamp %d (%d,%d) because of bad equation",
+                  index, (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
+    } else {
+        stamp->status = PM_SUBTRACTION_STAMP_USED;
+    }
+
+#ifdef TESTING
+    if (psTraceGetLevel("psModules.imcombine.equation") >= 10) {
+        psString matrixName = NULL;
+        psStringAppend(&matrixName, "matrix1_%d.fits", index);
+        psFits *matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, stamp->matrix1, 0, NULL);
+        psFitsClose(matrixFile);
+
+        matrixName = NULL;
+        psStringAppend(&matrixName, "vector1_%d.fits", index);
+        psImage *dummy = psImageAlloc(stamp->vector1->n, 1, PS_TYPE_F64);
+        memcpy(dummy->data.F64[0], stamp->vector1->data.F64,
+               PSELEMTYPE_SIZEOF(PS_TYPE_F64) * stamp->vector1->n);
+        matrixFile = psFitsOpen(matrixName, "w");
+        psFree(matrixName);
+        psFitsWriteImage(matrixFile, NULL, dummy, 0, NULL);
+        psFree(dummy);
+        psFitsClose(matrixFile);
+
+        if (stamp->vector2) {
+            matrixName = NULL;
+            psStringAppend(&matrixName, "vector2_%d.fits", index);
+            dummy = psImageAlloc(stamp->vector2->n, 1, PS_TYPE_F64);
+            memcpy(dummy->data.F64[0], stamp->vector2->data.F64,
+                   PSELEMTYPE_SIZEOF(PS_TYPE_F64) * stamp->vector2->n);
+            matrixFile = psFitsOpen(matrixName, "w");
+            psFree(matrixName);
+            psFitsWriteImage(matrixFile, NULL, dummy, 0, NULL);
+            psFree(dummy);
+            psFitsClose(matrixFile);
+        }
+
+        if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            matrixName = NULL;
+            psStringAppend(&matrixName, "matrix2_%d.fits", index);
+            matrixFile = psFitsOpen(matrixName, "w");
+            psFree(matrixName);
+            psFitsWriteImage(matrixFile, NULL, stamp->matrix2, 0, NULL);
+            psFitsClose(matrixFile);
+
+            matrixName = NULL;
+            psStringAppend(&matrixName, "matrixX_%d.fits", index);
+            matrixFile = psFitsOpen(matrixName, "w");
+            psFree(matrixName);
+            psFitsWriteImage(matrixFile, NULL, stamp->matrixX, 0, NULL);
+            psFitsClose(matrixFile);
+        }
+    }
+#endif
+
+    psFree(polyValues);
+
+    return true;
+}
+
+bool pmSubtractionCalculateEquation(pmSubtractionStampList *stamps, const pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+
+    psTimerStart("pmSubtractionCalculateEquation");
+
+    // We iterate over each stamp, allocate the matrix and vectors if
+    // necessary, and then calculate those matrix/vectors.
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE) {
+            continue;
+        }
+
+        if (pmSubtractionThreaded()) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_CALCULATE_EQUATION");
+            psArrayAdd(job->args, 1, stamps);
+            psArrayAdd(job->args, 1, (pmSubtractionKernels*)kernels); // Casting away const to put on array
+            PS_ARRAY_ADD_SCALAR(job->args, i, PS_TYPE_S32);
+            if (!psThreadJobAddPending(job)) {
+                psFree(job);
+                return false;
+            }
+            psFree(job);
+        } else {
+            pmSubtractionCalculateEquationStamp(stamps, kernels, i);
+        }
+    }
+
+    if (!psThreadPoolWait(true)) {
+        psError(PS_ERR_UNKNOWN, false, "Error waiting for threads.");
+        return false;
+    }
+
+    pmSubtractionVisualPlotLeastSquares(stamps);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Calculate equation: %f sec",
+             psTimerClear("pmSubtractionCalculateEquation"));
+
+
+    return true;
+}
+
+bool pmSubtractionSolveEquation(pmSubtractionKernels *kernels, const pmSubtractionStampList *stamps)
+{
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+
+    // Check inputs
+    int numParams = -1;                // Number of parameters
+    int numParams2 = 0;                // Number of parameters for part solution (DUAL mode)
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        PS_ASSERT_PTR_NON_NULL(stamp, false);
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            continue;
+        }
+
+        PS_ASSERT_VECTOR_NON_NULL(stamp->vector1, false);
+        if (numParams == -1) {
+            numParams = stamp->vector1->n;
+        }
+        PS_ASSERT_VECTOR_SIZE(stamp->vector1, (long)numParams, false);
+        PS_ASSERT_VECTOR_TYPE(stamp->vector1, PS_TYPE_F64, false);
+        PS_ASSERT_IMAGE_NON_NULL(stamp->matrix1, false);
+        PS_ASSERT_IMAGE_SIZE(stamp->matrix1, numParams, numParams, false);
+        PS_ASSERT_IMAGE_TYPE(stamp->matrix1, PS_TYPE_F64, false);
+
+        if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            PS_ASSERT_IMAGE_NON_NULL(stamp->matrix2, false);
+            PS_ASSERT_IMAGE_NON_NULL(stamp->matrixX, false);
+            if (numParams2 == 0) {
+                numParams2 = stamp->matrix2->numCols;
+            }
+            PS_ASSERT_IMAGE_SIZE(stamp->matrix2, numParams2, numParams2, false);
+            PS_ASSERT_IMAGE_SIZE(stamp->matrixX, numParams, numParams2, false);
+            PS_ASSERT_IMAGE_TYPE(stamp->matrix2, PS_TYPE_F64, false);
+            PS_ASSERT_IMAGE_TYPE(stamp->matrixX, PS_TYPE_F64, false);
+            PS_ASSERT_VECTOR_NON_NULL(stamp->vector2, false);
+            PS_ASSERT_VECTOR_SIZE(stamp->vector2, (long)numParams2, false);
+            PS_ASSERT_VECTOR_TYPE(stamp->vector2, PS_TYPE_F64, false);
+        }
+    }
+    if (numParams == -1) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "No suitable stamps found.");
+        return NULL;
+    }
+
+    if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+        // Accumulate the least-squares matricies and vectors
+        psImage *sumMatrix = psImageAlloc(numParams, numParams, PS_TYPE_F64); // Combined matrix
+        psVector *sumVector = psVectorAlloc(numParams, PS_TYPE_F64); // Combined vector
+        psVectorInit(sumVector, 0.0);
+        psImageInit(sumMatrix, 0.0);
+        int numStamps = 0;              // Number of good stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+                (void)psBinaryOp(sumMatrix, sumMatrix, "+", stamp->matrix1);
+                (void)psBinaryOp(sumVector, sumVector, "+", stamp->vector1);
+                numStamps++;
+            }
+        }
+        calculatePenalty(sumVector, kernels);
+
+#ifdef TESTING
+        {
+            psImage *inverse = psMatrixInvert(NULL, sumMatrix, NULL);
+            psFits *fits = psFitsOpen("matrixInv.fits", "w");
+            psFitsWriteImage(fits, NULL, inverse, 0, NULL);
+            psFitsClose(fits);
+            psFree(inverse);
+        }
+        {
+            psImage *X = psMatrixInvert(NULL, sumMatrix, NULL);
+            psImage *Xt = psMatrixTranspose(NULL, X);
+            psImage *XtX = psMatrixMultiply(NULL, Xt, X);
+            psFits *fits = psFitsOpen("matrixErr.fits", "w");
+            psFitsWriteImage(fits, NULL, XtX, 0, NULL);
+            psFitsClose(fits);
+            psFree(X);
+            psFree(Xt);
+            psFree(XtX);
+        }
+#endif
+
+        psVector *permutation = NULL;       // Permutation vector, required for LU decomposition
+        psImage *luMatrix = psMatrixLUD(NULL, &permutation, sumMatrix);
+        psFree(sumMatrix);
+        if (!luMatrix) {
+            psError(PS_ERR_UNKNOWN, true, "LU Decomposition of least-squares matrix failed.\n");
+            psFree(sumVector);
+            psFree(luMatrix);
+            psFree(permutation);
+            return NULL;
+        }
+        kernels->solution1 = psMatrixLUSolve(kernels->solution1, luMatrix, sumVector, permutation);
+
+        psFree(sumVector);
+        psFree(luMatrix);
+        psFree(permutation);
+        if (!kernels->solution1) {
+            psError(PS_ERR_UNKNOWN, true, "Failed to solve the least-squares system.\n");
+            return NULL;
+        }
+    } else {
+        // Dual convolution solution
+
+        // Accumulation of stamp matrices/vectors
+        psImage *sumMatrix1 = psImageAlloc(numParams, numParams, PS_TYPE_F64);
+        psImage *sumMatrix2 = psImageAlloc(numParams2, numParams2, PS_TYPE_F64);
+        psImage *sumMatrixX = psImageAlloc(numParams, numParams2, PS_TYPE_F64);
+        psVector *sumVector1 = psVectorAlloc(numParams, PS_TYPE_F64);
+        psVector *sumVector2 = psVectorAlloc(numParams, PS_TYPE_F64);
+        psImageInit(sumMatrix1, 0.0);
+        psImageInit(sumMatrix2, 0.0);
+        psImageInit(sumMatrixX, 0.0);
+        psVectorInit(sumVector1, 0.0);
+        psVectorInit(sumVector2, 0.0);
+
+        int numStamps = 0;              // Number of good stamps
+        for (int i = 0; i < stamps->num; i++) {
+            pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+            if (stamp->status == PM_SUBTRACTION_STAMP_USED) {
+                (void)psBinaryOp(sumMatrix1, sumMatrix1, "+", stamp->matrix1);
+                (void)psBinaryOp(sumMatrix2, sumMatrix2, "+", stamp->matrix2);
+                (void)psBinaryOp(sumMatrixX, sumMatrixX, "+", stamp->matrixX);
+                (void)psBinaryOp(sumVector1, sumVector1, "+", stamp->vector1);
+                (void)psBinaryOp(sumVector2, sumVector2, "+", stamp->vector2);
+                numStamps++;
+            }
+        }
+        calculatePenalty(sumVector1, kernels);
+        calculatePenalty(sumVector2, kernels);
+
+        // Pure matrix operations
+
+        // A * a = Ct * b + d
+        // C * a = B  * b + e
+        //
+        // a = (Ct * Bi * C - A)i (Ct * Bi * e - d)
+        // b = Bi * (C * a - e)
+        psVector *a = psVectorRecycle(kernels->solution1, numParams, PS_TYPE_F64);
+        psVector *b = psVectorRecycle(kernels->solution2, numParams2, PS_TYPE_F64);
+#ifdef TESTING
+        psVectorInit(a, NAN);
+        psVectorInit(b, NAN);
+#endif
+        psImage *A = sumMatrix1;
+        psImage *B = sumMatrix2;
+        psImage *C = sumMatrixX;
+        psVector *d = sumVector1;
+        psVector *e = sumVector2;
+
+        assert(a->n == numParams);
+        assert(b->n == numParams2);
+        assert(A->numRows == numParams && A->numCols == numParams);
+        assert(B->numRows == numParams2 && B->numCols == numParams2);
+        assert(C->numRows == numParams2 && C->numCols == numParams);
+        assert(d->n == numParams);
+        assert(e->n == numParams2);
+
+        psImage *Bi = psMatrixInvert(NULL, B, NULL);
+        assert(Bi->numRows == numParams2 && Bi->numCols == numParams2);
+        psImage *Ct = psMatrixTranspose(NULL, C);
+        assert(Ct->numRows == numParams && Ct->numCols == numParams2);
+
+        psImage *BiC = psMatrixMultiply(NULL, Bi, C);
+        assert(BiC->numRows == numParams2 && BiC->numCols == numParams);
+        psImage *CtBi = psMatrixMultiply(NULL, Ct, Bi);
+        assert(CtBi->numRows == numParams && CtBi->numCols == numParams2);
+
+        psImage *CtBiC = psMatrixMultiply(NULL, Ct, BiC);
+        assert(CtBiC->numRows == numParams && CtBiC->numCols == numParams);
+
+        psImage *F = (psImage*)psBinaryOp(NULL, CtBiC, "-", A);
+        assert(F->numRows == numParams && F->numCols == numParams);
+        float det = 0.0;
+        psImage *Fi = psMatrixInvert(NULL, F, &det);
+        assert(Fi->numRows == numParams && Fi->numCols == numParams);
+        psTrace("psModules.imcombine", 4, "Determinant of F: %f\n", det);
+
+        psVector *g = psVectorAlloc(numParams, PS_TYPE_F64);
+#ifdef TESTING
+        psVectorInit(g, NAN);
+#endif
+        assert(CtBi->numRows == numParams && CtBi->numCols == numParams2);
+        assert(e->n == numParams2);
+        assert(d->n == numParams);
+        for (int i = 0; i < numParams; i++) {
+            double value = 0.0;
+            for (int j = 0; j < numParams2; j++) {
+                value += CtBi->data.F64[i][j] * e->data.F64[j];
+            }
+            g->data.F64[i] = value - d->data.F64[i];
+        }
+
+        assert(Fi->numRows == numParams && Fi->numCols == numParams);
+        assert(g->n == numParams);
+        for (int i = 0; i < numParams; i++) {
+            double value = 0.0;
+            for (int j = 0; j < numParams; j++) {
+                value += Fi->data.F64[i][j] * g->data.F64[j];
+            }
+            a->data.F64[i] = value;
+        }
+
+        psVector *h = psVectorAlloc(numParams2, PS_TYPE_F64);
+#ifdef TESTING
+        psVectorInit(h, NAN);
+#endif
+        assert(C->numRows == numParams2 && C->numCols == numParams);
+        assert(a->n == numParams);
+        assert(e->n == numParams2);
+        for (int i = 0; i < numParams2; i++) {
+            double value = 0.0;
+            for (int j = 0; j < numParams; j++) {
+                value += C->data.F64[i][j] * a->data.F64[j];
+            }
+            h->data.F64[i] = value - e->data.F64[i];
+        }
+
+        assert(Bi->numRows == numParams2 && Bi->numCols == numParams2);
+        assert(h->n == numParams2);
+        for (int i = 0; i < numParams2; i++) {
+            double value = 0.0;
+            for (int j = 0; j < numParams2; j++) {
+                value += Bi->data.F64[i][j] * h->data.F64[j];
+            }
+            b->data.F64[i] = value;
+        }
+
+
+#if 0
+        for (int i = 0; i < numParams; i++) {
+            double aVal1 = 0.0, bVal1 = 0.0;
+            for (int j = 0; j < numParams2; j++) {
+                aVal1 += A->data.F64[i][j] * a->data.F64[j];
+                bVal1 += Ct->data.F64[i][j] * b->data.F64[j];
+            }
+            bVal1 += d->data.F64[i];
+            for (int j = numParams2; j < numParams; j++) {
+                aVal1 += A->data.F64[i][j] * a->data.F64[j];
+            }
+            printf("%d: %lf\n", i, aVal1 - bVal1);
+        }
+
+        for (int i = 0; i < numParams2; i++) {
+            double aVal2 = 0.0, bVal2 = 0.0;
+            for (int j = 0; j < numParams2; j++) {
+                aVal2 += C->data.F64[i][j] * a->data.F64[j];
+                bVal2 += B->data.F64[i][j] * b->data.F64[j];
+            }
+            bVal2 += e->data.F64[i];
+            for (int j = numParams2; j < numParams; j++) {
+                aVal2 += C->data.F64[i][j] * a->data.F64[j];
+            }
+            printf("%d: %lf\n", i, aVal2 - bVal2);
+        }
+#endif
+
+#ifdef TESTING
+        {
+            psFits *fits = psFitsOpen("sumMatrix1.fits", "w");
+            psFitsWriteImage(fits, NULL, sumMatrix1, 0, NULL);
+            psFitsClose(fits);
+        }
+        {
+            psFits *fits = psFitsOpen("sumMatrix2.fits", "w");
+            psFitsWriteImage(fits, NULL, sumMatrix2, 0, NULL);
+            psFitsClose(fits);
+        }
+        {
+            psFits *fits = psFitsOpen("sumMatrixX.fits", "w");
+            psFitsWriteImage(fits, NULL, sumMatrixX, 0, NULL);
+            psFitsClose(fits);
+        }
+        {
+            psFits *fits = psFitsOpen("sumFinverse.fits", "w");
+            psFitsWriteImage(fits, NULL, Fi, 0, NULL);
+            psFitsClose(fits);
+        }
+#endif
+
+        kernels->solution1 = a;
+        kernels->solution2 = b;
+
+        // XXXXX Free temporary matrices and vectors
+
+    }
+
+    if (psTraceGetLevel("psModules.imcombine") >= 7) {
+        for (int i = 0; i < kernels->solution1->n; i++) {
+            psTrace("psModules.imcombine", 7, "Solution 1 %d: %f\n", i, kernels->solution1->data.F64[i]);
+        }
+        if (kernels->mode == PM_SUBTRACTION_MODE_DUAL) {
+            for (int i = 0; i < kernels->solution2->n; i++) {
+                psTrace("psModules.imcombine", 7, "Solution 2 %d: %f\n", i, kernels->solution2->data.F64[i]);
+            }
+        }
+     }
+
+    pmSubtractionVisualPlotLeastSquares((pmSubtractionStampList *) stamps); //casting away const
+    return true;
+}
+
+psVector *pmSubtractionCalculateDeviations(pmSubtractionStampList *stamps,
+                                           const pmSubtractionKernels *kernels)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_NON_NULL(kernels, NULL);
+    PM_ASSERT_SUBTRACTION_KERNELS_SOLUTION(kernels, NULL);
+
+    psVector *deviations = psVectorAlloc(stamps->num, PS_TYPE_F32); // Mean deviation for stamps
+    int footprint = stamps->footprint; // Half-size of stamps
+    long numPixels = PS_SQR(2 * footprint + 1); // Number of pixels in footprint
+    double devNorm = 1.0 / (double)numPixels; // Normalisation for deviations
+    int numKernels = kernels->num;      // Number of kernels
+
+    psImage *polyValues = NULL;         // Polynomial values
+    psKernel *residual = psKernelAlloc(-footprint, footprint, -footprint, footprint); // Residual image
+
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // The stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            deviations->data.F32[i] = NAN;
+            continue;
+        }
+
+        // Calculate coefficients of the kernel basis functions
+        polyValues = p_pmSubtractionPolynomial(polyValues, kernels->spatialOrder, stamp->xNorm, stamp->yNorm);
+        double norm = p_pmSubtractionSolutionNorm(kernels); // Normalisation
+        double background = p_pmSubtractionSolutionBackground(kernels, polyValues);// Difference in background
+
+        // Calculate residuals
+        psKernel *variance = stamp->variance; // Variance postage stamp
+        psImageInit(residual->image, 0.0);
+        if (kernels->mode != PM_SUBTRACTION_MODE_DUAL) {
+            psKernel *target;           // Target postage stamp
+            psKernel *source;           // Source postage stamp
+            psArray *convolutions;      // Convolution postage stamps for each kernel basis function
+            switch (kernels->mode) {
+              case PM_SUBTRACTION_MODE_1:
+                target = stamp->image2;
+                source = stamp->image1;
+                convolutions = stamp->convolutions1;
+                break;
+              case PM_SUBTRACTION_MODE_2:
+                target = stamp->image1;
+                source = stamp->image2;
+                convolutions = stamp->convolutions2;
+                break;
+              default:
+                psAbort("Unsupported subtraction mode: %x", kernels->mode);
+            }
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *convolution = convolutions->data[j]; // Convolution
+                double coefficient = p_pmSubtractionSolutionCoeff(kernels, polyValues, j,
+                                                                  false); // Coefficient
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] -= convolution->kernel[y][x] * coefficient;
+                    }
+                }
+            }
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += target->kernel[y][x] - background - source->kernel[y][x] * norm;
+                }
+            }
+        } else {
+            // Dual convolution
+            psArray *convolutions1 = stamp->convolutions1; // Convolutions of the first image
+            psArray *convolutions2 = stamp->convolutions2; // Convolutions of the second image
+            psKernel *image1 = stamp->image1; // The first image
+            psKernel *image2 = stamp->image2; // The second image
+
+            for (int j = 0; j < numKernels; j++) {
+                psKernel *conv1 = convolutions1->data[j]; // Convolution of first image
+                psKernel *conv2 = convolutions2->data[j]; // Convolution of second image
+                double coeff1 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, false); // Coefficient 1
+                double coeff2 = p_pmSubtractionSolutionCoeff(kernels, polyValues, j, true); // Coefficient 2
+
+                for (int y = - footprint; y <= footprint; y++) {
+                    for (int x = - footprint; x <= footprint; x++) {
+                        residual->kernel[y][x] += conv2->kernel[y][x] * coeff2 - conv1->kernel[y][x] * coeff1;
+                    }
+                }
+            }
+            for (int y = - footprint; y <= footprint; y++) {
+                for (int x = - footprint; x <= footprint; x++) {
+                    residual->kernel[y][x] += image2->kernel[y][x] - background - image1->kernel[y][x] * norm;
+                }
+            }
+        }
+
+        double deviation = 0.0;         // Sum of differences
+        for (int y = - footprint; y <= footprint; y++) {
+            for (int x = - footprint; x <= footprint; x++) {
+                double dev = PS_SQR(residual->kernel[y][x]) / variance->kernel[y][x];
+                deviation += dev;
+#ifdef TESTING
+                residual->kernel[y][x] = dev;
+#endif
+            }
+        }
+        deviations->data.F32[i] = devNorm * deviation;
+        psTrace("psModules.imcombine", 5, "Deviation for stamp %d (%d,%d): %f\n",
+                i, (int)(stamp->x + 0.5), (int)(stamp->y + 0.5), deviations->data.F32[i]);
+        if (!isfinite(deviations->data.F32[i])) {
+            stamp->status = PM_SUBTRACTION_STAMP_REJECTED;
+            psTrace("psModules.imcombine", 5,
+                    "Rejecting stamp %d (%d,%d) because of non-finite deviation\n",
+                    i, (int)(stamp->x + 0.5), (int)(stamp->y + 0.5));
+            continue;
+        }
+
+#ifdef TESTING
+        {
+            psString filename = NULL;
+            psStringAppend(&filename, "resid_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, residual->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->image1) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_image1_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->image1->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->image2) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_image2_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->image2->image, 0, NULL);
+            psFitsClose(fits);
+        }
+        if (stamp->variance) {
+            psString filename = NULL;
+            psStringAppend(&filename, "stamp_variance_%03d.fits", i);
+            psFits *fits = psFitsOpen(filename, "w");
+            psFree(filename);
+            psFitsWriteImage(fits, NULL, stamp->variance->image, 0, NULL);
+            psFitsClose(fits);
+        }
+#endif
+
+    }
+    psFree(residual);
+    psFree(polyValues);
+
+    return deviations;
+}
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionMatch.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionMatch.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionMatch.c	(revision 21432)
@@ -0,0 +1,689 @@
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmSubtractionParams.h"
+#include "pmSubtractionKernels.h"
+#include "pmSubtractionStamps.h"
+#include "pmSubtractionEquation.h"
+#include "pmSubtraction.h"
+#include "pmSubtractionAnalysis.h"
+#include "pmSubtractionMask.h"
+#include "pmSubtractionThreads.h"
+#include "pmSubtractionMatch.h"
+#include "pmSubtractionVisual.h"
+
+#define BG_STAT PS_STAT_ROBUST_MEDIAN   // Statistic to use for background
+
+static bool useFFT = true;              // Do convolutions using FFT
+
+
+//#define TESTING
+//#define TESTING_MEMORY
+
+// Output memory usage information
+static void memCheck(const char *where)
+{
+#ifdef TESTING_MEMORY
+    psMemBlock **leaks = NULL;
+    int numLeaks = psMemCheckLeaks(0, &leaks, NULL, true);
+    size_t largestSize = 0;
+    psMemId largest = 0;
+    size_t totalSize = 0;
+    for (int i = 0; i < numLeaks; i++) {
+        psMemBlock *mb = leaks[i];
+        totalSize += mb->userMemorySize;
+        if (mb->userMemorySize > largestSize) {
+            largestSize = mb->userMemorySize;
+            largest = mb->id;
+        }
+    }
+    psFree(leaks);
+    fprintf(stderr, "%s:\n", where);
+    fprintf(stderr, "    Memory in use: %zd\n", totalSize);
+    fprintf(stderr, "    Largest block: %ld\n", largest);
+    fprintf(stderr, "    sbrk(): %zd\n", (size_t)sbrk(0));
+#endif
+    return;
+}
+
+
+static bool getStamps(pmSubtractionStampList **stamps, // Stamps to read
+                      const pmReadout *ro1, // Readout 1
+                      const pmReadout *ro2, // Readout 2
+                      const psImage *subMask, // Mask for subtraction, or NULL
+                      psImage *variance,  // Variance map
+                      const psRegion *region, // Region of interest, or NULL
+                      float threshold,  // Threshold for stamp finding
+                      float stampSpacing, // Spacing between stamps
+                      int size,         // Kernel half-size
+                      int footprint,     // Convolution footprint for stamps
+                      pmSubtractionMode mode // Mode for subtraction
+    )
+{
+    psTrace("psModules.imcombine", 3, "Finding stamps...\n");
+    *stamps = pmSubtractionStampsFind(*stamps, ro1->image, subMask, region, threshold, footprint,
+                                      stampSpacing, mode);
+    if (!*stamps) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to find stamps.");
+        return false;
+    }
+
+    memCheck("  find stamps");
+
+    psTrace("psModules.imcombine", 3, "Extracting stamps...\n");
+    if (!pmSubtractionStampsExtract(*stamps, ro1->image, ro2 ? ro2->image : NULL, variance, size)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to extract stamps.");
+        return false;
+    }
+
+    memCheck("   extract stamps");
+    pmSubtractionVisualPlotStamps(*stamps, (pmReadout *) ro1);
+    return true;
+}
+
+
+bool pmSubtractionMatch(pmReadout *conv1, pmReadout *conv2, const pmReadout *ro1, const pmReadout *ro2,
+                        int footprint, int stride, float regionSize, float stampSpacing, float threshold,
+                        const psArray *sources, const char *stampsName,
+                        pmSubtractionKernelsType type, int size, int spatialOrder,
+                        const psVector *isisWidths, const psVector *isisOrders,
+                        int inner, int ringsOrder, int binning, float penalty,
+                        bool optimum, const psVector *optFWHMs, int optOrder, float optThreshold,
+                        int iter, float rej, float sysError, psImageMaskType maskVal, psImageMaskType maskBad,
+                        psImageMaskType maskPoor, float poorFrac, float badFrac, pmSubtractionMode subMode)
+{
+    if (subMode != PM_SUBTRACTION_MODE_2) {
+        PM_ASSERT_READOUT_NON_NULL(conv1, false);
+        if (conv1->image) {
+            psFree(conv1->image);
+            conv1->image = NULL;
+        }
+        if (conv1->mask) {
+            psFree(conv1->mask);
+            conv1->mask = NULL;
+        }
+        if (conv1->variance) {
+            psFree(conv1->variance);
+            conv1->variance = NULL;
+        }
+    }
+    if (subMode != PM_SUBTRACTION_MODE_1) {
+        PM_ASSERT_READOUT_NON_NULL(conv2, false);
+        if (conv2->image) {
+            psFree(conv2->image);
+            conv2->image = NULL;
+        }
+        if (conv2->mask) {
+            psFree(conv2->mask);
+            conv2->mask = NULL;
+        }
+        if (conv2->variance) {
+            psFree(conv2->variance);
+            conv2->variance = NULL;
+        }
+    }
+
+    PM_ASSERT_READOUT_NON_NULL(ro1, false);
+    PM_ASSERT_READOUT_NON_NULL(ro2, false);
+    PM_ASSERT_READOUT_IMAGE(ro1, false);
+    PM_ASSERT_READOUT_IMAGE(ro2, false);
+    PS_ASSERT_IMAGES_SIZE_EQUAL(ro1->image, ro2->image, false);
+
+    PS_ASSERT_INT_NONNEGATIVE(footprint, false);
+    PS_ASSERT_INT_NONNEGATIVE(stride, false);
+    // regionSize can be just about anything (except maybe negative, but it can be NAN)
+    PS_ASSERT_FLOAT_LARGER_THAN(stampSpacing, 0.0, false);
+    // Don't care what threshold is
+    if (sources) {
+        PS_ASSERT_ARRAY_NON_NULL(sources, false);
+    }
+    // stampsName may be anything
+    // We'll check kernel type when we allocate the kernels
+    PS_ASSERT_INT_POSITIVE(size, false);
+    PS_ASSERT_INT_NONNEGATIVE(spatialOrder, false);
+    if (isisWidths || isisOrders) {
+        PS_ASSERT_VECTOR_NON_NULL(isisWidths, false);
+        PS_ASSERT_VECTOR_TYPE(isisWidths, PS_TYPE_F32, false);
+        PS_ASSERT_VECTOR_NON_NULL(isisOrders, false);
+        PS_ASSERT_VECTOR_TYPE(isisOrders, PS_TYPE_S32, false);
+        PS_ASSERT_VECTORS_SIZE_EQUAL(isisWidths, isisOrders, false);
+    }
+    PS_ASSERT_INT_NONNEGATIVE(inner, false);
+    PS_ASSERT_INT_NONNEGATIVE(ringsOrder, false);
+    PS_ASSERT_INT_POSITIVE(binning, false);
+    if (optimum) {
+        PS_ASSERT_VECTOR_NON_NULL(optFWHMs, false);
+        PS_ASSERT_INT_NONNEGATIVE(optOrder, false);
+        PS_ASSERT_FLOAT_LARGER_THAN(optThreshold, 0.0, false);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(optThreshold, 1.0, false);
+    }
+    PS_ASSERT_INT_POSITIVE(iter, false);
+    PS_ASSERT_FLOAT_LARGER_THAN(rej, 0.0, false);
+    if (isfinite(sysError)) {
+        PS_ASSERT_FLOAT_LARGER_THAN_OR_EQUAL(sysError, 0.0, NULL);
+        PS_ASSERT_FLOAT_LESS_THAN(sysError, 1.0, NULL);
+    }
+    // Don't care about maskVal
+    // Don't care about maskBad
+    // Don't care about maskPoor
+    PS_ASSERT_FLOAT_LARGER_THAN(poorFrac, 0.0, NULL);
+    PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(poorFrac, 1.0, NULL);
+    if (isfinite(badFrac)) {
+        PS_ASSERT_FLOAT_LARGER_THAN(badFrac, 0.0, NULL);
+        PS_ASSERT_FLOAT_LESS_THAN_OR_EQUAL(badFrac, 1.0, NULL);
+    }
+
+    // If the stamp footprint is smaller than the kernel size, then we won't get much signal in the outer
+    // parts of the kernel, which can result in bad matching artifacts.
+    if (footprint < size) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true,
+                "Stamp footprint (%d) should be larger than or equal to the kernel size (%d)",
+                footprint, size);
+        return false;
+    }
+
+    // Where does our variance map come from?
+    // Getting the variance exactly right is not necessary --- it's just used for weighting.
+    psImage *variance = NULL;             // Variance image to use
+    if (ro1->variance && ro2->variance) {
+        variance = (psImage*)psBinaryOp(NULL, ro1->variance, "+", ro2->variance);
+    } else if (ro1->variance) {
+        variance = psMemIncrRefCounter(ro1->variance);
+    } else if (ro2->variance) {
+        variance = psMemIncrRefCounter(ro2->variance);
+    } else {
+        variance = (psImage*)psBinaryOp(NULL, ro1->image, "+", ro2->image);
+    }
+
+    // Putting important variable declarations here, since they are freed after a "goto" if there is an error.
+    psImage *subMask = NULL;            // Mask for subtraction
+    psRegion *region = NULL;            // Iso-kernel region
+    psString regionString = NULL;       // String for region
+    pmSubtractionStampList *stamps = NULL; // Stamps for matching PSF
+    pmSubtractionKernels *kernels = NULL; // Kernel basis functions
+    psMetadata *analysis = psMetadataAlloc(); // QA data
+
+    int numCols = ro1->image->numCols, numRows = ro1->image->numRows; // Image dimensions
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS, 0); // Random number generator
+
+    memCheck("start");
+
+    subMask = pmSubtractionMask(ro1->mask, ro2 ? ro2->mask : NULL, maskVal, size, footprint,
+                                badFrac, useFFT);
+    if (!subMask) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate subtraction mask.");
+        goto MATCH_ERROR;
+    }
+
+    memCheck("mask");
+
+    // Get region of interest
+    int xRegions = 1, yRegions = 1;     // Number of iso-kernel regions
+    float xRegionSize = 0, yRegionSize = 0; // Size of iso-kernel regions
+    if (isfinite(regionSize) && regionSize != 0.0) {
+        xRegions = numCols / regionSize + 1;
+        yRegions = numRows / regionSize + 1;
+        xRegionSize = (float)numCols / (float)xRegions;
+        yRegionSize = (float)numRows / (float)yRegions;
+        region = psRegionAlloc(NAN, NAN, NAN, NAN);
+    }
+
+    {
+        psStats *bg = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV); // Statistics for backgroun
+        if (!psImageBackground(bg, NULL, ro1->image, ro1->mask, maskVal, rng)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to measure background statistics.");
+            psFree(bg);
+            psFree(rng);
+            goto MATCH_ERROR;
+        }
+        threshold = bg->robustMedian + threshold * bg->robustStdev;
+        psFree(bg);
+    }
+
+    // Iterate over iso-kernel regions
+    for (int j = 0; j < yRegions; j++) {
+        for (int i = 0; i < xRegions; i++) {
+            psTrace("psModules.imcombine", 1, "Subtracting region %d of %d...\n",
+                    j * xRegions + i + 1, xRegions * yRegions);
+            if (region) {
+                *region = psRegionSet((int)(i * xRegionSize), (int)((i + 1) * xRegionSize),
+                                      (int)(j * yRegionSize), (int)((j + 1) * yRegionSize));
+                psFree(regionString);
+                regionString = psRegionToString(*region);
+                psTrace("psModules.imcombine", 3, "Iso-kernel region: %s out of %d,%d\n",
+                        regionString, numCols, numRows);
+            }
+
+            if (sources) {
+                stamps = pmSubtractionStampsSetFromSources(sources, ro1->image, subMask, region, footprint,
+                                                           stampSpacing, subMode);
+            } else if (stampsName && strlen(stampsName) > 0) {
+                stamps = pmSubtractionStampsSetFromFile(stampsName, ro1->image, subMask, region, footprint,
+                                                        stampSpacing, subMode);
+            }
+
+            // We get the stamps here; we will also attempt to get stamps at the first iteration, but it
+            // doesn't matter.
+            if (!getStamps(&stamps, ro1, ro2, subMask, variance, NULL, threshold, stampSpacing,
+                           size, footprint, subMode)) {
+                goto MATCH_ERROR;
+            }
+
+            if (subMode == PM_SUBTRACTION_MODE_UNSURE) {
+                // Get backgrounds
+                psStats *bgStats = psStatsAlloc(BG_STAT); // Statistics for background
+                psVector *buffer = NULL;// Buffer for stats
+                if (!psImageBackground(bgStats, &buffer, ro1->image, ro1->mask, maskVal, rng)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to measure background of image 1.");
+                    psFree(bgStats);
+                    psFree(buffer);
+                    goto MATCH_ERROR;
+                }
+                float bg1 = psStatsGetValue(bgStats, BG_STAT); // Background for image 1
+                if (!psImageBackground(bgStats, &buffer, ro2->image, ro2->mask, maskVal, rng)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to measure background of image 2.");
+                    psFree(bgStats);
+                    psFree(buffer);
+                    goto MATCH_ERROR;
+                }
+                float bg2 = psStatsGetValue(bgStats, BG_STAT); // Background for image 2
+                psFree(bgStats);
+                psFree(buffer);
+
+                pmSubtractionMode newMode = pmSubtractionOrder(stamps, bg1, bg2); // Subtraction mode to use
+                switch (newMode) {
+                  case PM_SUBTRACTION_MODE_1:
+                    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolving image 1 to match image 2.");
+                    break;
+                  case PM_SUBTRACTION_MODE_2:
+                    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Convolving image 2 to match image 1.");
+                    break;
+                  default:
+                    psError(PS_ERR_UNKNOWN, false, "Unable to determine subtraction order.");
+                    goto MATCH_ERROR;
+                }
+                subMode = newMode;
+            }
+
+            // Define kernel basis functions
+            if (optimum && (type == PM_SUBTRACTION_KERNEL_ISIS || type == PM_SUBTRACTION_KERNEL_GUNK)) {
+                kernels = pmSubtractionKernelsOptimumISIS(type, size, inner, spatialOrder, optFWHMs, optOrder,
+                                                          stamps, footprint, optThreshold, penalty, subMode);
+                if (!kernels) {
+                    psErrorClear();
+                    psWarning("Unable to derive optimum ISIS kernel --- switching to default.");
+                }
+            }
+            if (kernels == NULL) {
+                // Not an ISIS/GUNK kernel, or the optimum kernel search failed
+                kernels = pmSubtractionKernelsGenerate(type, size, spatialOrder, isisWidths, isisOrders,
+                                                       inner, binning, ringsOrder, penalty, subMode);
+            }
+
+            memCheck("kernels");
+
+            int numRejected = -1;       // Number of rejected stamps in each iteration
+            for (int k = 0; k < iter && numRejected != 0; k++) {
+                psLogMsg("psModules.imcombine", PS_LOG_INFO, "Iteration %d.", k);
+
+                if (!getStamps(&stamps, ro1, ro2, subMask, variance, region, threshold, stampSpacing,
+                               size, footprint, subMode)) {
+                    goto MATCH_ERROR;
+                }
+
+                psTrace("psModules.imcombine", 3, "Calculating equation...\n");
+                if (!pmSubtractionCalculateEquation(stamps, kernels)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto MATCH_ERROR;
+                }
+
+                memCheck("  calculate equation");
+
+                psTrace("psModules.imcombine", 3, "Solving equation...\n");
+
+                if (!pmSubtractionSolveEquation(kernels, stamps)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto MATCH_ERROR;
+                }
+
+                memCheck("  solve equation");
+
+                psVector *deviations = pmSubtractionCalculateDeviations(stamps, kernels); // Stamp deviations
+                if (!deviations) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate deviations.");
+                    goto MATCH_ERROR;
+                }
+
+                memCheck("   calculate deviations");
+
+                psTrace("psModules.imcombine", 3, "Rejecting stamps...\n");
+                numRejected = pmSubtractionRejectStamps(kernels, stamps, deviations, subMask, rej, footprint);
+                if (numRejected < 0) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to reject stamps.");
+                    psFree(deviations);
+                    goto MATCH_ERROR;
+                }
+                psFree(deviations);
+
+                memCheck("  reject stamps");
+            }
+
+            if (numRejected > 0) {
+                psTrace("psModules.imcombine", 3, "Solving equation...\n");
+                if (!pmSubtractionSolveEquation(kernels, stamps)) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate least-squares equation.");
+                    goto MATCH_ERROR;
+                }
+                psVector *deviations = pmSubtractionCalculateDeviations(stamps, kernels); // Stamp deviations
+                if (!deviations) {
+                    psError(PS_ERR_UNKNOWN, false, "Unable to calculate deviations.");
+                    goto MATCH_ERROR;
+                }
+                pmSubtractionRejectStamps(kernels, stamps, deviations, subMask, NAN, footprint);
+                psFree(deviations);
+            }
+            psFree(stamps);
+            stamps = NULL;
+
+            memCheck("solution");
+
+            if (!pmSubtractionAnalysis(analysis, kernels, region, numCols, numRows)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to generate QA data");
+                goto MATCH_ERROR;
+            }
+
+            memCheck("diag outputs");
+
+            psTrace("psModules.imcombine", 2, "Convolving...\n");
+            if (!pmSubtractionConvolve(conv1, conv2, ro1, ro2, subMask, stride, maskBad, maskPoor, poorFrac,
+                                       sysError, region, kernels, true, useFFT)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to convolve image.");
+                goto MATCH_ERROR;
+            }
+
+            psFree(kernels);
+            kernels = NULL;
+
+            // There is data in the readout now
+            if (subMode == PM_SUBTRACTION_MODE_1 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+                conv1->data_exists = true;
+                if (conv1->parent) {
+                    conv1->parent->data_exists = true;
+                    conv1->parent->parent->data_exists = true;
+                }
+            }
+            if (subMode == PM_SUBTRACTION_MODE_2 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+                conv2->data_exists = true;
+                if (conv2->parent) {
+                    conv2->parent->data_exists = true;
+                    conv2->parent->parent->data_exists = true;
+                }
+            }
+        }
+    }
+    psFree(rng);
+    rng = NULL;
+    psFree(region);
+    region = NULL;
+    psFree(regionString);
+    regionString = NULL;
+    psFree(subMask);
+    subMask = NULL;
+    psFree(variance);
+    variance = NULL;
+
+    if (!pmSubtractionBorder(conv1->image, conv1->variance, conv1->mask, size, maskBad)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to set border of convolved image.");
+        goto MATCH_ERROR;
+    }
+
+    memCheck("convolution");
+
+    if (conv1) {
+        psMetadataCopy(conv1->analysis, analysis);
+    }
+    if (conv2) {
+        psMetadataCopy(conv2->analysis, analysis);
+    }
+    psFree(analysis);
+
+#ifdef TESTING
+    {
+        if (subMode == PM_SUBTRACTION_MODE_1 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+            psFits *fits = psFitsOpen("convolved1.fits", "w");
+            psFitsWriteImage(fits, NULL, conv1->image, 0, NULL);
+            psFitsClose(fits);
+        }
+
+        if (subMode == PM_SUBTRACTION_MODE_2 || subMode == PM_SUBTRACTION_MODE_DUAL) {
+            psFits *fits = psFitsOpen("convolved2.fits", "w");
+            psFitsWriteImage(fits, NULL, conv2->image, 0, NULL);
+            psFitsClose(fits);
+        }
+    }
+#endif
+
+    return true;
+
+MATCH_ERROR:
+    psFree(analysis);
+    psFree(region);
+    psFree(regionString);
+    psFree(subMask);
+    psFree(kernels);
+    psFree(stamps);
+    psFree(variance);
+    psFree(rng);
+    return false;
+}
+
+
+// Determine a rough width (integer value) of the star in the image
+// XXX Could improve this by using a user-provided list of floating-point widths (or an end point and
+// increment).
+static int subtractionOrderWidth(const psKernel *kernel, // Image
+                                 float bg, // Background in image
+                                 int size, // Maximum size
+                                 const psArray *models, // Buffer of models
+                                 const psVector *modelSums // Buffer of model sums
+    )
+{
+    assert(kernel);
+    assert(models);
+    assert(modelSums);
+
+    int xMin = -size, xMax = size; // Bounds in x
+    int yMin = -size, yMax = size; // Bounds in y
+
+    // Fit gaussians of varying widths to the image, record the chi^2
+    psVector *chi2 = psVectorAlloc(size, PS_TYPE_F32); // chi^2 as a function of radius
+    for (int sigma = 0; sigma < size; sigma++) {
+        double sumFG = 0.0; // Sum for calculating the normalisation of the Gaussian
+        psKernel *model = models->data[sigma]; // Model of interest
+        for (int y = yMin; y <= yMax; y++) {
+            for (int x = xMin; x <= xMax; x++) {
+                sumFG += model->kernel[y][x] * (kernel->kernel[y][x] - bg);
+            }
+        }
+        float norm = sumFG * modelSums->data.F64[sigma]; // Normalisation for Gaussian
+        double sumDev2 = 0.0;           // Sum of square deviations
+        for (int y = yMin; y <= yMax; y++) {
+            for (int x = xMin; x <= xMax; x++) {
+                float dev = kernel->kernel[y][x] - bg - norm * model->kernel[y][x]; // Deviation
+                sumDev2 += PS_SQR(dev);
+            }
+        }
+        chi2->data.F32[sigma] = sumDev2;
+    }
+
+    // Find the minimum chi^2
+    int bestIndex = -1;                 // Index of best chi^2
+    float bestChi2 = INFINITY;          // Best chi^2
+    for (int i = 0; i < size; i++) {
+        if (chi2->data.F32[i] < bestChi2) {
+            bestChi2 = chi2->data.F32[i];
+            bestIndex = i;
+        }
+    }
+    psFree(chi2);
+
+    return bestIndex + 1;
+}
+
+
+bool pmSubtractionOrderStamp(psVector *ratios, psVector *mask, const pmSubtractionStampList *stamps,
+                             const psArray *models, const psVector *modelSums,
+                             int index, float bg1, float bg2)
+{
+    PS_ASSERT_VECTOR_NON_NULL(ratios, false);
+    PS_ASSERT_VECTOR_NON_NULL(mask, false);
+    PS_ASSERT_VECTORS_SIZE_EQUAL(ratios, mask, false);
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, false);
+    PS_ASSERT_INT_NONNEGATIVE(index, false);
+    PS_ASSERT_INT_LESS_THAN(index, stamps->num, false);
+    PS_ASSERT_ARRAY_NON_NULL(models, false);
+    PS_ASSERT_VECTOR_NON_NULL(modelSums, false);
+    PS_ASSERT_VECTOR_SIZE(modelSums, models->n, false);
+
+    pmSubtractionStamp *stamp = stamps->stamps->data[index]; // Stamp of interest
+    psAssert(stamp->status == PM_SUBTRACTION_STAMP_CALCULATE || stamp->status == PM_SUBTRACTION_STAMP_USED,
+             "We checked this earlier.");
+
+    // Widths of stars
+    int width1 = subtractionOrderWidth(stamp->image1, bg1, stamps->footprint, models, modelSums);
+    int width2 = subtractionOrderWidth(stamp->image2, bg2, stamps->footprint, models, modelSums);
+
+    if (width1 == 0 || width2 == 0) {
+        ratios->data.F32[index] = NAN;
+        mask->data.PS_TYPE_VECTOR_MASK_DATA[index] = 0xff;
+    } else {
+        ratios->data.F32[index] = (float)width1 / (float)width2;
+        mask->data.PS_TYPE_VECTOR_MASK_DATA[index] = 0;
+        psTrace("psModules.imcombine", 3, "Stamp %d (%.1f,%.1f) widths: %d, %d --> %f\n",
+                index, stamp->x, stamp->y, width1, width2, ratios->data.F32[index]);
+    }
+
+    return true;
+}
+
+bool pmSubtractionOrderThread(psThreadJob *job)
+{
+    PS_ASSERT_THREAD_JOB_NON_NULL(job, false);
+
+    psVector *ratios = job->args->data[0]; // Ratios of widths
+    psVector *mask = job->args->data[1]; // Mask for ratios
+    const pmSubtractionStampList *stamps = job->args->data[2]; // List of stamps
+    const psArray *models = job->args->data[3]; // Gaussian models
+    const psVector *modelSums = job->args->data[4]; // Gaussian model sums
+    int index = PS_SCALAR_VALUE(job->args->data[5], S32); // Stamp index
+    float bg1 = PS_SCALAR_VALUE(job->args->data[6], F32); // Background of image 1
+    float bg2 = PS_SCALAR_VALUE(job->args->data[7], F32); // Background of image 2
+
+    return pmSubtractionOrderStamp(ratios, mask, stamps, models, modelSums, index, bg1, bg2);
+}
+
+pmSubtractionMode pmSubtractionOrder(pmSubtractionStampList *stamps, float bg1, float bg2)
+{
+    PM_ASSERT_SUBTRACTION_STAMP_LIST_NON_NULL(stamps, PM_SUBTRACTION_MODE_ERR);
+
+    psVector *mask = psVectorAlloc(stamps->num, PS_TYPE_VECTOR_MASK); // Mask for stamps
+    psVector *ratios = psVectorAlloc(stamps->num, PS_TYPE_F32); // Ratios of widths
+
+    // Generate models
+    int size = stamps->footprint;       // Maximum size
+    psArray *models = psArrayAlloc(size); // Gaussian models
+    psVector *modelSums = psVectorAlloc(size, PS_TYPE_F64); // Gaussian model sums
+    for (int sigma = 0; sigma < size; sigma++) {
+        psKernel *model = psKernelAlloc(-size, size, -size, size); // Gaussian model
+        float invSigma2 = 1.0 / (float)PS_SQR(1 + sigma); // Inverse sigma squared
+        double sumGG = 0.0;         // Sum of square of Gaussian
+        for (int y = -size; y <= size; y++) {
+            int y2 = PS_SQR(y);     // y squared
+            for (int x = -size; x <= size; x++) {
+                float rad2 = PS_SQR(x) + y2; // Radius squared
+                float value = expf(-rad2 * invSigma2); // Model value
+                model->kernel[y][x] = value;
+                sumGG += PS_SQR(value);
+            }
+        }
+        models->data[sigma] = model;
+        modelSums->data.F64[sigma] = 1.0 / sumGG;
+    }
+
+    // Fit models to stamps
+    for (int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i]; // Stamp of interest
+        if (stamp->status != PM_SUBTRACTION_STAMP_CALCULATE && stamp->status != PM_SUBTRACTION_STAMP_USED) {
+            mask->data.PS_TYPE_VECTOR_MASK_DATA[i] = 0xff;
+            continue;
+        }
+
+        if (pmSubtractionThreaded()) {
+            psThreadJob *job = psThreadJobAlloc("PSMODULES_SUBTRACTION_ORDER");
+            psArrayAdd(job->args, 1, ratios);
+            psArrayAdd(job->args, 1, mask);
+            psArrayAdd(job->args, 1, stamps);
+            psArrayAdd(job->args, 1, models);
+            psArrayAdd(job->args, 1, modelSums);
+            PS_ARRAY_ADD_SCALAR(job->args, i, PS_TYPE_S32);
+            PS_ARRAY_ADD_SCALAR(job->args, bg1, PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, bg2, PS_TYPE_F32);
+            if (!psThreadJobAddPending(job)) {
+                psFree(job);
+                return false;
+            }
+            psFree(job);
+        } else {
+            if (!pmSubtractionOrderStamp(ratios, mask, stamps, models, modelSums, i, bg1, bg2)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to measure PSF width for stamp %d", i);
+                psFree(models);
+                psFree(modelSums);
+                psFree(ratios);
+                psFree(mask);
+                return false;
+            }
+        }
+    }
+
+    if (!psThreadPoolWait(true)) {
+        psError(PS_ERR_UNKNOWN, false, "Error waiting for threads.");
+        psFree(models);
+        psFree(modelSums);
+        psFree(ratios);
+        psFree(mask);
+            return false;
+    }
+
+    psFree(models);
+    psFree(modelSums);
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+    if (!psVectorStats(stats, ratios, NULL, mask, 0xff)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to calculate statistics for moments ratio.");
+        psFree(mask);
+        psFree(ratios);
+        psFree(stats);
+        return PM_SUBTRACTION_MODE_ERR;
+    }
+    psFree(ratios);
+    psFree(mask);
+
+    psLogMsg("psModules.imcombine", PS_LOG_INFO, "Median width ratio: %lf", stats->robustMedian);
+    pmSubtractionMode mode = (stats->robustMedian <= 1.0 ? PM_SUBTRACTION_MODE_1 : PM_SUBTRACTION_MODE_2);
+    psFree(stats);
+
+    // XXX EAM : I think Paul left some test code in here.  I've commented these lines out
+    // return PM_SUBTRACTION_MODE_2;
+    // exit(1);
+
+    return mode;
+}
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.c
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.c	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.c	(revision 21432)
@@ -0,0 +1,275 @@
+/** Diagnostic plots for pmSubtraction
+ * @author Chris Beaumont, IfA
+ */
+
+/* Include Files   */
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <strings.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+#include <pslib.h>
+
+#include "pmKapaPlots.h"
+#include "pmSubtractionStamps.h"
+
+#include "pmVisual.h"
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryObjects.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+//variables to determine when things are plotted
+static bool isVisual             = false;
+static bool plotConvKernels      = true;
+static bool plotStamps           = true;
+static bool plotLeastSquares     = true;
+static bool plotImage            = true;
+// variables to store plotting window indices
+static int kapa  = -1;
+static int kapa2 = -1;
+
+/** function prototypes*/
+static bool plotStampLocations(pmSubtractionStampList *stamps, pmReadout *ro);
+
+// Initialization Routines
+
+/** start or stop plotting */
+bool pmSubtractionSetVisual (bool mode) {
+    isVisual = mode;
+    return true;
+}
+
+
+/** destroy windows at the end of a run*/
+bool pmSubtractionVisualClose()
+{
+    if(kapa != -1)
+        KiiClose(kapa);
+    if(kapa2 != -1)
+        KiiClose(kapa2);
+    return true;
+}
+
+// Plotting Routines
+
+/** Display images of the convolution kernels
+ *  @param convKernels the kernels to plot
+ *    @return true for success */
+bool pmSubtractionVisualPlotConvKernels(psImage *convKernels) {
+    if (!isVisual || !plotConvKernels) return true;
+    if (!pmVisualInitWindow(&kapa, "ppSub:Images")) {
+        isVisual = false;
+        return false;
+    }
+    pmVisualScaleImage(kapa, convKernels, "Convolution_Kernels", 0, true);
+    pmVisualAskUser(&plotConvKernels, &isVisual);
+    return true;
+}
+
+
+/** Display the postage stamps used to determine the convolution kernels
+    @param stamps to display
+    @return true for success */
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro) {
+    if (!isVisual || !plotStamps) return true;
+    if (!pmVisualInitWindow (&kapa, "ppSub:Images")) {
+        isVisual = false;
+        return false;
+    }
+    if (!pmVisualInitWindow (&kapa2, "ppSub:StampMasterImage")) {
+        isVisual = false;
+        return false;
+    }
+
+    //Plot Location of stamps
+    plotStampLocations(stamps, ro);
+
+    //Find the stamp size
+    int imageMax = -1;
+    int numStamps = 0;
+    psElemType type = PS_TYPE_F32; // will be overwritten
+    for(int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        psKernel *k1 = stamp->image1;
+        if (k1 == NULL) continue;
+
+        psImage *i1 = k1->image;
+        if (i1 == NULL) continue;
+
+        imageMax = PS_MAX(imageMax, i1->numCols);
+        imageMax = PS_MAX(imageMax, i1->numRows);
+        numStamps++;
+        type = i1->type.type;
+    }
+    if (imageMax == -1) return false;
+
+    int border = 30;
+    int tileRowCount = (int) ceil(sqrt(numStamps));
+    int canvasX = tileRowCount * (imageMax * 2 + border);
+    int canvasY = tileRowCount * (imageMax + border);
+    psImage *canvas = psImageAlloc (canvasX, canvasY, type);
+    psImageInit (canvas, NAN);
+
+    //overlay the images
+    int stampNum = 0;
+    int stampListNum = 0;
+    while (stampNum < numStamps) {
+        int x0 = (2 * imageMax + border) * (stampNum % tileRowCount);
+        int y0 = (imageMax + border) * (stampNum / tileRowCount);
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[stampListNum++];
+        if (stamp == NULL) continue;
+
+        psKernel *k1 = stamp->image1;
+        psKernel *k2 = stamp->image2;
+
+        if (k1 == NULL) continue;
+        psImage *i1 = k1->image;
+        psImage *i2 = k2->image;
+
+        if (i1 == NULL) continue;
+        psImageOverlaySection(canvas, i1, x0 + 0 * imageMax, y0, "=");
+        psImageOverlaySection(canvas, i2, x0 + 1 * imageMax + border / 4, y0, "=");
+        stampNum++;
+    }
+    pmVisualScaleImage(kapa, canvas, "Subtraction_Stamps", 0, true);
+
+    pmVisualAskUser(&plotStamps, &isVisual);
+    return true;
+}
+
+/** Plot the least-squares matrix of each stamp */
+bool pmSubtractionVisualPlotLeastSquares (pmSubtractionStampList *stamps, bool dual) {
+
+    if (!isVisual || !plotLeastSquares) return true;
+    if (!pmVisualInitWindow (&kapa, "PPSub:Images")) {
+        isVisual = false;
+        return false;
+    }
+
+    //Find the stamp size
+    int imageMax = -1;
+    int numStamps = 0;
+    psElemType type = PS_TYPE_F64;
+
+    for(int i = 0; i < stamps->num; i++) {
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix1;
+        if (im == NULL) im = stamp->matrix2;
+        if (im == NULL) im = stamp->matrixX;
+        if (im == NULL) continue;
+
+        imageMax = PS_MAX(imageMax, im->numCols);
+        imageMax = PS_MAX(imageMax, im->numRows);
+        numStamps++;
+        type = im->type.type;
+    }
+    if (imageMax == -1) return false;
+
+    int border = 15;
+    imageMax += border;
+    int tileRowCount = (int) ceil(sqrt(numStamps));
+    int canvasX = tileRowCount * (imageMax);
+    int canvasY = tileRowCount * (imageMax);
+    psImage *canvas = psImageAlloc (canvasX, canvasY, type);
+    psImageInit (canvas, NAN);
+
+    //overlay the images
+    int stampNum = 0;
+    int stampListNum = 0;
+    while (stampNum < numStamps) {
+        int x0 = (imageMax) * (stampNum % tileRowCount);
+        int y0 = (imageMax) * (stampNum / tileRowCount);
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[stampListNum++];
+        if (stamp == NULL) continue;
+
+        psImage *im = stamp->matrix1;
+        if (im == NULL) im = stamp->matrix2;
+        if (im == NULL) im = stamp->matrixX;
+        if (im == NULL) continue;
+
+        psImageOverlaySection(canvas, im, x0, y0, "=");
+        stampNum++;
+    }
+
+    psImage *canvas32 = pmVisualImageToFloat(canvas);
+    pmVisualScaleImage(kapa, canvas32, "Least_Squares", 0, true);
+
+    pmVisualAskUser(&plotLeastSquares, &isVisual);
+    psFree(canvas);
+    psFree(canvas32);
+    return true;
+}
+
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub) {
+    if (!isVisual || !plotImage) return true;
+    if (!pmVisualInitWindow (&kapa, "PPSub:Images")) {
+        isVisual = false;
+        return false;
+    }
+
+    pmVisualScaleImage(kapa, image, "Image", 0, true);
+    pmVisualScaleImage(kapa, ref, "Reference", 1, true);
+    pmVisualScaleImage(kapa, sub, "Subtraction", 2, true);
+    pmVisualAskUser(&plotImage, &isVisual);
+    return true;
+}
+
+static bool plotStampLocations(pmSubtractionStampList *stamps, pmReadout *ro) {
+
+    if (!pmVisualScaleImage(kapa2, ro->image, "Stamp_master_image", 0, true)) {
+        fprintf(stderr, "Cannot display postage stamp master image. Skipping \n");
+        return false;
+    }
+
+    int Noverlay;
+    KiiOverlay *overlay;
+
+    // note: this uses the Ohana allocation tools:
+    // ALLOCATE (overlay, KiiOverlay, 3*peaks->n + 1);
+    ALLOCATE (overlay, KiiOverlay, stamps->num);
+
+    Noverlay = 0;
+    char boxID[10];
+    for (int i = 0; i < stamps->num; i++) {
+
+        pmSubtractionStamp *stamp = stamps->stamps->data[i];
+        if (stamp == NULL) continue;
+
+        overlay[Noverlay].type = KII_OVERLAY_BOX;
+        overlay[Noverlay].x = stamp->x;
+        overlay[Noverlay].y = stamp->y;
+        overlay[Noverlay].dx = 40.0;
+        overlay[Noverlay].dy = 40.0;
+        overlay[Noverlay].angle = 0.0;
+        sprintf(boxID, "%d", i);
+        overlay[Noverlay].text = boxID;
+        Noverlay ++;
+    }
+
+    KiiLoadOverlay (kapa2, overlay, Noverlay, "red");
+    FREE (overlay);
+    return true;
+}
+
+#else
+bool pmSubtractionSetVisual (bool mode) {return true;}
+bool pmSubtractionVisualClose() {return true;}
+bool pmSubtractionVisualPlotConvKernels(psImage *convKernels) {return true;}
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro) {return true;}
+bool pmSubtractionVisualPlotLeastSquares(pmSubtractionStampList *stamps) {return true;}
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub) {return true;}
+#endif
Index: /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.h
===================================================================
--- /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.h	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/imcombine/pmSubtractionVisual.h	(revision 21432)
@@ -0,0 +1,11 @@
+#ifndef PM_SUBTRACTION_VISUAL_H
+#define PM_SUBTRACTION_VISUAL_H
+
+bool pmSubtractionSetVisual (bool mode);
+bool pmSubtractionVisualClose();
+bool pmSubtractionVisualPlotConvKernels(psImage *convKernels);
+bool pmSubtractionVisualPlotStamps(pmSubtractionStampList *stamps, pmReadout *ro);
+bool pmSubtractionVisualPlotLeastSquares(pmSubtractionStampList *stamps);
+bool pmSubtractionVisualShowSubtraction(psImage *image, psImage *ref, psImage *sub);
+
+#endif
Index: /branches/eam_branch_20090208/psModules/src/psmodules.h
===================================================================
--- /branches/eam_branch_20090208/psModules/src/psmodules.h	(revision 21432)
+++ /branches/eam_branch_20090208/psModules/src/psmodules.h	(revision 21432)
@@ -0,0 +1,134 @@
+#ifndef PS_MODULES_H
+#define PS_MODULES_H
+
+#include <pslib.h>
+
+// the following headers are from psModule:extras
+#include <psPipe.h>
+#include <psIOBuffer.h>
+#include <psVectorBracket.h>
+#include <pmKapaPlots.h>
+#include <pmVisual.h>
+
+// XXX the following headers define constructs needed by the elements below
+#include <pmConfig.h>
+#include <pmDetrendDB.h>
+#include <pmDetrendThreads.h>
+#include <pmHDU.h>
+#include <pmFPA.h>
+#include <pmFPALevel.h>
+#include <pmFPAview.h>
+#include <pmFPAfile.h>
+
+// the following headers are from psModule:config
+#include <pmErrorCodes.h>
+#include <pmConfigRecipes.h>
+#include <pmConfigCamera.h>
+#include <pmConfigCommand.h>
+#include <pmConfigMask.h>
+#include <pmConfigDump.h>
+#include <pmVersion.h>
+
+// the following headers are from psModule:concepts
+#include <pmConcepts.h>
+#include <pmConceptsRead.h>
+#include <pmConceptsStandard.h>
+#include <pmConceptsWrite.h>
+#include <pmConceptsPhotcode.h>
+#include <pmConceptsAverage.h>
+#include <pmConceptsUpdate.h>
+
+// the following headers are from psModule:camera
+#include <pmHDUUtils.h>
+#include <pmHDUGenerate.h>
+#include <pmFPAFlags.h>
+#include <pmFPAfileDefine.h>
+#include <pmFPAfileFitsIO.h>
+#include <pmFPAfileIO.h>
+#include <pmFPARead.h>
+#include <pmFPAConstruct.h>
+#include <pmFPACopy.h>
+#include <pmFPAHeader.h>
+#include <pmFPAMaskWeight.h>
+#include <pmFPAMosaic.h>
+#include <pmFPARead.h>
+#include <pmFPAWrite.h>
+#include <pmFPA_JPEG.h>
+#include <pmFPAExtent.h>
+#include <pmFPACalibration.h>
+#include <pmReadoutStack.h>
+#include <pmFPAUtils.h>
+#include <pmCellSquish.h>
+#include <pmFPABin.h>
+
+// the following headers are from psModule:detrend
+#include <pmFlatField.h>
+#include <pmFlatNormalize.h>
+#include <pmFringeStats.h>
+#include <pmMaskBadPixels.h>
+#include <pmNonLinear.h>
+#include <pmOverscan.h>
+#include <pmBias.h>
+#include <pmShutterCorrection.h>
+// #include <pmSkySubtract.h>
+#include <pmDark.h>
+#include <pmRemnance.h>
+
+// the following headers are from psModule:astrom
+#include <pmAstrometryWCS.h>
+#include <pmAstrometryUtils.h>
+#include <pmAstrometryRegions.h>
+#include <pmAstrometryObjects.h>
+#include <pmAstrometryModel.h>
+#include <pmAstrometryRefstars.h>
+#include <pmAstrometryDistortion.h>
+#include <pmAstrometryVisual.h>
+
+// the following headers are from psModule:imcombine
+#include <pmStack.h>
+#include <pmStackReject.h>
+#include <pmSubtraction.h>
+#include <pmSubtractionThreads.h>
+#include <pmSubtractionStamps.h>
+#include <pmSubtractionKernels.h>
+#include <pmSubtractionAnalysis.h>
+#include <pmSubtractionMatch.h>
+#include <pmSubtractionIO.h>
+#include <pmSubtractionParams.h>
+#include <pmSubtractionMask.h>
+#include <pmSubtractionEquation.h>
+#include <pmReadoutCombine.h>
+#include <pmSubtractionVisual.h>
+
+// the following headers are from psModule:objects
+#include <pmSpan.h>
+#include <pmFootprint.h>
+#include <pmPeaks.h>
+#include <pmDetections.h>
+#include <pmMoments.h>
+#include <pmResiduals.h>
+#include <pmGrowthCurve.h>
+#include <pmTrend2D.h>
+#include <pmPSF.h>
+#include <pmModel.h>
+#include <pmSource.h>
+#include <pmSourceUtils.h>
+#include <pmSourceIO.h>
+#include <pmSourceSky.h>
+#include <pmSourceFitModel.h>
+#include <pmSourceFitSet.h>
+#include <pmSourceContour.h>
+#include <pmSourcePlots.h>
+#include <pmPSF_IO.h>
+#include <pmPSFtry.h>
+#include <pmModelClass.h>
+#include <pmModelUtils.h>
+#include <pmSourcePhotometry.h>
+#include <pmSourceVisual.h>
+#include <pmSourceMatch.h>
+
+// The following headers are from random locations, here because they cross bounds
+#include <pmReadoutFake.h>
+#include <pmPSFEnvelope.h>
+
+#endif
