Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/.cvsignore
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/.cvsignore	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/.cvsignore	(revision 22306)
@@ -0,0 +1,6 @@
+.deps
+.libs
+Makefile
+Makefile.in
+*.la
+*.lo
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/Makefile.am
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/Makefile.am	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/Makefile.am	(revision 22306)
@@ -0,0 +1,23 @@
+noinst_LTLIBRARIES = libpsmodulesastrom.la
+
+libpsmodulesastrom_la_CPPFLAGS = $(SRCINC) $(PSMODULES_CFLAGS) -I../pslib/
+libpsmodulesastrom_la_LDFLAGS  = -release $(PACKAGE_VERSION)
+libpsmodulesastrom_la_SOURCES  = \
+	pmAstrometryObjects.c \
+	pmAstrometryRegions.c \
+	pmAstrometryDistortion.c \
+	pmAstrometryUtils.c \
+	pmAstrometryModel.c \
+	pmAstrometryRefstars.c \
+	pmAstrometryWCS.c
+
+pkginclude_HEADERS = \
+	pmAstrometryObjects.h \
+	pmAstrometryRegions.h \
+	pmAstrometryDistortion.h \
+	pmAstrometryUtils.h \
+	pmAstrometryModel.h \
+	pmAstrometryRefstars.h \
+	pmAstrometryWCS.h
+
+CLEANFILES = *~
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.c	(revision 22306)
@@ -0,0 +1,352 @@
+/** @file  pmAstrometryDistortion.c
+*
+*  @brief This file defines the basic types for measuring the focal-plane distortion.
+*
+*  @ingroup AstroImage
+*
+*  @author EAM, IfA
+*
+*  @version $Revision: 1.22 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2008-09-02 19:05:09 $
+*
+*  Copyright 2006 Institute for Astronomy, University of Hawaii
+*/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+/******************************************************************************/
+/*  INCLUDE FILES                                                             */
+/******************************************************************************/
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryRegions.h"
+#include "pmAstrometryDistortion.h"
+#include "pmKapaPlots.h"
+
+static void pmAstromGradientFree (pmAstromGradient *grad)
+{
+
+    if (grad == NULL)
+        return;
+
+    return;
+}
+
+pmAstromGradient *pmAstromGradientAlloc ()
+{
+
+    pmAstromGradient *gradient = psAlloc (sizeof(pmAstromGradient));
+    psMemSetDeallocator(gradient, (psFreeFunc) pmAstromGradientFree);
+
+    return (gradient);
+}
+
+psArray *pmAstromMeasureGradients(psArray *gradients, psArray *rawstars, psArray *refstars, psArray *matches, psRegion *region, int Nx, int Ny)
+{
+
+    if (gradients == NULL) {
+        gradients = psArrayAllocEmpty (100);
+    }
+
+    // NOTE: region specifies the FP region in pixels covered by the chip (NOT in FP units)
+    // determine range
+    int DX = (region->x1 - region->x0) / Nx;
+    int DY = (region->y1 - region->y0) / Ny;
+
+    psPolynomial2D *local = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, 1, 1);
+    local->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // measure gradient for fractional chip regions
+    for (int nx = 0; nx < Nx; nx++) {
+        for (int ny = 0; ny < Ny; ny++) {
+            int Xmin = nx*DX;
+            int Xmax = Xmin + DX;
+            int Ymin = ny*DY;
+            int Ymax = Ymin + DY;
+
+            psStats *stats = NULL;
+            psVector *mask = NULL;
+            pmAstromGradient *grad = NULL;
+
+            psVector *L  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *M  = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dP = psVectorAllocEmpty (100, PS_TYPE_F32);
+            psVector *dQ = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+            // XXX this is a bit inefficient: first sorting by X or Y could speed this up.
+            // XXX or assigning to a segment in a single pass first
+            // select the stars within this chip region
+            int Npts = 0;
+            for (int i = 0; i < matches->n; i++) {
+
+                pmAstromMatch *match = matches->data[i];
+
+                pmAstromObj *raw = rawstars->data[match->raw];
+
+                if (raw->chip->x < Xmin) continue;
+                if (raw->chip->x > Xmax) continue;
+                if (raw->chip->y < Ymin) continue;
+                if (raw->chip->y > Ymax) continue;
+
+                pmAstromObj *ref = refstars->data[match->ref];
+
+                L->data.F32[Npts] = raw->FP->x;
+                M->data.F32[Npts] = raw->FP->y;
+
+                // P,Q = L,M + terms of order epsilon.
+                // measuring the gradient constrains thos terms
+                dP->data.F32[Npts] = ref->TP->x - raw->FP->x;
+                dQ->data.F32[Npts] = ref->TP->y - raw->FP->y;
+
+                psVectorExtend (L, 100, 1);
+                psVectorExtend (M, 100, 1);
+                psVectorExtend (dP, 100, 1);
+                psVectorExtend (dQ, 100, 1);
+                Npts++;
+            }
+
+            psTrace ("psModules.astrom", 4, "Npts: %d (%d,%d) : (%d - %d),(%d - %d)\n", Npts, nx, ny, Xmin, Xmax, Ymin, Ymax);
+
+            if (Npts < 5)
+                goto skip;
+
+            // stats structure and mask for use in measuring the clipping statistic
+            // this analysis has too few data points to use the robust median method
+            stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+            mask = psVectorAlloc (Npts, PS_TYPE_MASK);
+            psVectorInit (mask, 0);
+
+            grad = pmAstromGradientAlloc ();
+
+	    // XXX psTraceSetLevel("psLib.math.psVectorClipFitPolynomial2D", 7);
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dP, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.x = local->coeff[1][0];
+            grad->dTPdM.x = local->coeff[0][1];
+
+	    // XXX psTraceSetLevel("psLib.math.psVectorClipFitPolynomial2D", 0);
+
+            // fit the collection of positions and offsets with a local 1st order gradient
+            // apply 3hi/3lo sigma clipping to the fitted data values
+            // the mask is used to mark the points which pass / fail the fit
+            if (!psVectorClipFitPolynomial2D (local, stats, mask, 0xff, dQ, NULL, L, M)) {
+                goto skip;
+            }
+
+            grad->dTPdL.y = local->coeff[1][0];
+            grad->dTPdM.y = local->coeff[0][1];
+
+            // also measure the L and M median positions as a representative coordinate
+            psVectorStats (stats, L, NULL, NULL, 0);
+            grad->FP.x = stats->sampleMedian;
+
+            psVectorStats (stats, M, NULL, NULL, 0);
+            grad->FP.y = stats->sampleMedian;
+
+            psArrayAdd (gradients, 100, grad);
+
+skip:
+            psFree (grad);
+            psFree (stats);
+            psFree (mask);
+            psFree (L);
+            psFree (M);
+            psFree (dP);
+            psFree (dQ);
+        }
+    }
+    psFree (local);
+    return gradients;
+}
+
+bool pmAstromFitDistortion(pmFPA *fpa, psArray *gradients, double pixelScale)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_ARRAY_NON_NULL(gradients, false);
+
+    psPolynomial2D *localX = NULL;
+    psPolynomial2D *localY = NULL;
+
+    // assign the gradient elements to psVectors for fitting
+    psVector *dPdL = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dQdL = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dPdM = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *dQdM = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *L = psVectorAlloc (gradients->n, PS_TYPE_F32);
+    psVector *M = psVectorAlloc (gradients->n, PS_TYPE_F32);
+
+    for (int i = 0; i < gradients->n; i++) {
+
+        pmAstromGradient *grad = gradients->data[i];
+
+        dPdL->data.F32[i] = grad->dTPdL.x;
+        dQdL->data.F32[i] = grad->dTPdL.y;
+
+        dPdM->data.F32[i] = grad->dTPdM.x;
+        dQdM->data.F32[i] = grad->dTPdM.y;
+
+        L->data.F32[i] = grad->FP.x;
+        M->data.F32[i] = grad->FP.y;
+    }
+
+    // mask and stats structure for measuring the clipping statistic
+    // this analysis has too few data points to use the robust median method
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psVector *mask = psVectorAlloc (gradients->n, PS_TYPE_MASK);
+    psVectorInit (mask, 0);
+
+    // the order of the gradient fits need to be 1 less than the distortion term
+    // determine the gradient order(s) from the fpa->toTP structure
+    localX = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->x->nX-1, fpa->toTPA->x->nY);
+    localY = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->x->nX,   fpa->toTPA->x->nY-1);
+
+    // set masks based on fpa->toTPA
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            if ((i > 0) && (i <= fpa->toTPA->x->nX)) {
+                localX->coeffMask[i-1][j] = fpa->toTPA->x->coeffMask[i][j];
+            }
+            if ((j > 0) && (j <= fpa->toTPA->x->nY)) {
+                localY->coeffMask[i][j-1] = fpa->toTPA->x->coeffMask[i][j];
+            }
+        }
+    }
+
+    // fit the local gradients in each direction
+    if (!psVectorClipFitPolynomial2D (localX, stats, mask, 0xff, dPdL, NULL, L, M)) {
+        psLogMsg ("psastro", 3, "failed to fit x-dir gradient\n");
+        psFree (localX);
+        psFree (localY);
+        goto escape;
+    }
+
+    if (!psVectorClipFitPolynomial2D (localY, stats, mask, 0xff, dPdM, NULL, L, M)) {
+        psLogMsg ("psastro", 3, "failed to fit y-dir gradient\n");
+        psFree (localX);
+        psFree (localY);
+        goto escape;
+    }
+
+    // update fpa->toTP distortion terms
+    fpa->toTPA->x->coeff[0][0] = 0;
+    for (int i = 1; i <= fpa->toTPA->x->nX; i++) {
+        if (fpa->toTPA->x->coeffMask[i][0] & PS_POLY_MASK_SET) {
+            continue;
+        }
+        fpa->toTPA->x->coeff[i][0] = localX->coeff[i-1][0] / i;
+    }
+    for (int j = 1; j <= fpa->toTPA->x->nY; j++) {
+        if (fpa->toTPA->x->coeffMask[0][j] & PS_POLY_MASK_SET) {
+            continue;
+        }
+        fpa->toTPA->x->coeff[0][j] = localY->coeff[0][j-1] / j;
+    }
+    for (int i = 1; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 1; j <= fpa->toTPA->x->nY; j++) {
+            if (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) {
+                continue;
+            }
+            fpa->toTPA->x->coeff[i][j] = 0.5*(localX->coeff[i-1][j] / i + localY->coeff[i][j-1] / j);
+        }
+    }
+    fpa->toTPA->x->coeff[1][0] += 1.0;
+    psFree (localX);
+    psFree (localY);
+
+    // the order of the gradient fits need to be 1 less than the distortion term
+    // determine the gradient order(s) from the fpa->toTP structure
+    localX = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->y->nX-1, fpa->toTPA->y->nY);
+    localY = psPolynomial2DAlloc (PS_POLYNOMIAL_ORD, fpa->toTPA->y->nX,   fpa->toTPA->y->nY-1);
+
+    // set masks based on fpa->toTP
+    for (int i = 0; i < fpa->toTPA->y->nX; i++) {
+        for (int j = 0; j < fpa->toTPA->y->nY; j++) {
+            if ((i > 0) && (i <= fpa->toTPA->y->nX)) {
+                localX->coeffMask[i-1][j] = fpa->toTPA->y->coeffMask[i][j];
+            }
+            if ((j > 0) && (j <= fpa->toTPA->y->nY)) {
+                localY->coeffMask[i][j-1] = fpa->toTPA->y->coeffMask[i][j];
+            }
+        }
+    }
+
+    // fit the local gradients in each direction
+    psVectorClipFitPolynomial2D (localX, stats, mask, 0xff, dQdL, NULL, L, M);
+    psVectorClipFitPolynomial2D (localY, stats, mask, 0xff, dQdM, NULL, L, M);
+
+    // update fpa->toTP distortion terms
+    fpa->toTPA->y->coeff[0][0] = 0;
+    for (int i = 1; i <= fpa->toTPA->y->nX; i++) {
+        if (fpa->toTPA->y->coeffMask[i][0] & PS_POLY_MASK_SET) {
+            continue;
+        }
+        fpa->toTPA->y->coeff[i][0] = localX->coeff[i-1][0] / i;
+    }
+    for (int j = 1; j <= fpa->toTPA->y->nY; j++) {
+        if (fpa->toTPA->y->coeffMask[0][j] & PS_POLY_MASK_SET) {
+            continue;
+        }
+        fpa->toTPA->y->coeff[0][j] = localY->coeff[0][j-1] / j;
+    }
+    for (int i = 1; i <= fpa->toTPA->y->nX; i++) {
+        for (int j = 1; j <= fpa->toTPA->y->nY; j++) {
+            if (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) {
+                continue;
+            }
+            fpa->toTPA->y->coeff[i][j] = 0.5*(localX->coeff[i-1][j] / i + localY->coeff[i][j-1] / j);
+        }
+    }
+    fpa->toTPA->y->coeff[0][1] += 1.0;
+    psFree (localX);
+    psFree (localY);
+
+    // free unneeded structures
+    psFree (dPdL);
+    psFree (dPdM);
+    psFree (dQdL);
+    psFree (dQdM);
+    psFree (L);
+    psFree (M);
+    psFree (stats);
+    psFree (mask);
+
+    // reset the fromTPA terms here. choose an appropriate region based on the dimensions of
+    // the complete FPA
+    psRegion *region = pmAstromFPAExtent (fpa);
+
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, *region, 50);
+    psFree (region);
+
+    if (fpa->fromTPA == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert fpa->toTPA\n");
+        return false;
+    }
+
+    return true;
+
+escape:
+    // free unneeded structures
+    psFree (dPdL);
+    psFree (dPdM);
+    psFree (dQdL);
+    psFree (dQdM);
+    psFree (L);
+    psFree (M);
+    psFree (stats);
+    psFree (mask);
+    return false;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryDistortion.h	(revision 22306)
@@ -0,0 +1,59 @@
+/* @file  pmAstrometryDistortion.h
+ * @brief This file defines the basic types for measuring and fitting the focal-plane distortion.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-18 22:07:17 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_DISTORTION_H
+#define PM_ASTROMETRY_DISTORTION_H
+
+/// @addtogroup Astrometry
+/// @{
+
+/* The following data structure carries the information about the residual
+ * gradient of source positions in the tangent plane (pmAstromObj.TP) as a
+ * function of position in the focal plane (pmAstromObj.FP).
+ */
+typedef struct
+{
+    psPlane FP;
+    psPlane dTPdL;
+    psPlane dTPdM;
+}
+pmAstromGradient;
+
+pmAstromGradient *pmAstromGradientAlloc ();
+
+/* The following function determines the position residual, in the tangent
+ * plane, as a function of position in the focal plane, for a collection of raw
+ * measurements and matched reference stars. The configuration data must include
+ * the bin size over which the gradient is measured (keyword: ASTROM.GRAD.BOX).
+ * The function returns an array of pmAstromGradient structures, defined below.
+ */
+psArray *pmAstromMeasureGradients(
+    psArray *gradients,
+    psArray *rawstars,
+    psArray *refstars,
+    psArray *matches,
+    psRegion *region,
+    int Nx, int Ny
+);
+
+/* The gradient set measured above can be fitted with a pair of 2D
+ * polynomials. The resulting fits can then be related back to the implied
+ * polynomials which represent the distortion. The following function performs
+ * the fit and applies the result to the distortion transformation of the
+ * supplied pmFPA structure. The configuration variable supplies the polynomial
+ * order (keyword: ASTROM.DISTORT.ORDER).
+ */
+bool pmAstromFitDistortion(
+    pmFPA *fpa,
+    psArray *gradients,
+    double pixelScale);
+
+/// @}
+#endif // PM_ASTROMETRY_DISTORTION_H
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.c	(revision 22306)
@@ -0,0 +1,763 @@
+/** @file  pmAstrometryModel.c
+ *
+ *  @brief Functions to read and write astrometric model
+ *
+ *  The generic model does not specify the location of the boresite on the sky, and it includes
+ *  a model for the rotator and motion of the boresite.
+ *
+ *  @ingroup AstroImage
+ *
+ *  @author EAM, IfA
+ *  @version $Revision: 1.6 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-09-17 23:07:02 $
+ *
+ *  Copyright 2007 Institute for Astronomy, 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 "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAExtent.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmAstrometryWCS.h"
+#include "pmAstrometryUtils.h"
+#include "pmAstrometryRegions.h"
+#include "pmAstrometryModel.h"
+
+# define REQUIRE(TEST,MESSAGE){ if (!(TEST)) { psAbort (MESSAGE); }}
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmAstromModelCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmAstromModelCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmAstromModelCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "Astrometry only valid at the chip level");
+    return false;
+}
+
+bool pmAstromModelCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    if (!fpa->toTPA) return false;
+    if (!fpa->fromTPA) return false;
+    if (!fpa->toSky) return false;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmAstromModelCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+bool pmAstromModelCheckDataStatusForChip (const pmChip *chip) {
+
+    if (!chip->toFPA) return false;
+    if (!chip->fromFPA) return false;  // XXX not strictly needed?
+    return true;
+}
+
+/********************* Write Data functions *****************************/
+
+bool pmAstromModelWriteForView (const pmFPAview *view, pmFPAfile *file, pmConfig *config)
+{
+    // write the full model in one pass: require the level to be FPA
+    if (view->chip != -1) {
+        psError(PS_ERR_IO, false, "Astrometry must be written at the FPA level");
+        return false;
+    }
+
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+
+    if (!pmAstromModelWriteFPA(file, fpa)) {
+        psError(PS_ERR_IO, false, "Failed to write Astrometry for fpa");
+        psFree(fpa);
+        return false;
+    }
+
+    psFree(fpa);
+
+    return true;
+}
+
+// write out all chip-level Astrometry data for this FPA
+bool pmAstromModelWriteFPA (pmFPAfile *file, const pmFPA *fpa)
+{
+
+
+    if (!pmAstromModelWritePHU (file, fpa)) {
+        psError(PS_ERR_IO, false, "Failed to write PHU for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Astrometry for chips");
+        return false;
+    }
+
+    if (!pmAstromModelWriteFP (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteTP (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelWriteSky (file)) {
+        psError(PS_ERR_IO, false, "Failed to write Sky for Astrometry model");
+        return false;
+    }
+
+    return true;
+}
+
+bool pmAstromModelWritePHU (pmFPAfile *file, const pmFPA *fpa) {
+    // Need to have an FPA suitable for writing, so that the headers are all kosher
+
+    // output header data
+    psMetadata *outhead = psMetadataAlloc();
+
+    // use the FPA phu to generate the PHU header
+    pmHDU *phu = fpa->hdu;
+
+    // if there is no FPA PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+    }
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+// fourth layer holds the chips
+bool pmAstromModelWriteChips (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "CHIPS");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "POLYNOMIAL");
+
+    psArray *model = psArrayAllocEmpty (1);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    pmChip *chip = NULL;
+    while ((chip = pmFPAviewNextChip (view, file->fpa, 1)) != NULL) {
+
+        if (!chip->toFPA) continue;
+        assert (chip->toFPA->x);
+        assert (chip->toFPA->y);
+
+        psRegion *region = pmChipPixels (chip);
+
+        // set the chip name
+        char *chiprule = psStringCopy ("{CHIP.NAME}");
+        char *chipname = pmFPAfileNameFromRule (chiprule, file, view);
+
+        for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+            for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+                psMetadata *row = psMetadataAlloc ();
+
+                psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", chipname);
+                psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+                psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", i);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", j);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", chip->toFPA->x->nX);
+                psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", chip->toFPA->x->nY);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", chip->toFPA->x->coeff[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", chip->toFPA->y->coeff[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", chip->toFPA->x->coeffErr[i][j]);
+                psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", chip->toFPA->y->coeffErr[i][j]);
+                psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", chip->toFPA->x->coeffMask[i][j]);
+                psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", chip->toFPA->y->coeffMask[i][j]);
+                psArrayAdd (model, 100, row);
+                psFree (row);
+            }
+        }
+        psFree (chiprule);
+        psFree (chipname);
+        psFree (region);
+    }
+
+    if (!psFitsWriteTable (file->fits, header, model, "CHIPS")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        return false;
+    }
+
+    psFree (view);
+    psFree (model);
+    psFree (header);
+    return true;
+}
+
+// third layer is the focal plane
+bool pmAstromModelWriteFP (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "FOCAL_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "TANGENT_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "POLYNOMIAL");
+
+    psArray *model = psArrayAllocEmpty (1);
+
+    // region over which the fromTPA projection is valid
+    psRegion *region = pmAstromFPInTP (file->fpa);
+
+    psPlaneTransform *toTPA   = file->fpa->toTPA;
+
+    for (int i = 0; i <= toTPA->x->nX; i++) {
+        for (int j = 0; j <= toTPA->x->nY; j++) {
+            psMetadata *row = psMetadataAlloc ();
+            psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "FOCAL_PLANE");
+            psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "TANGENT_PLANE");
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+            psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", i);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", j);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", toTPA->x->nX);
+            psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", toTPA->x->nY);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", toTPA->x->coeff[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", toTPA->y->coeff[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", toTPA->x->coeffErr[i][j]);
+            psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", toTPA->y->coeffErr[i][j]);
+            psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", toTPA->x->coeffMask[i][j]);
+            psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", toTPA->y->coeffMask[i][j]);
+
+            psArrayAdd (model, 100, row);
+            psFree (row);
+        }
+    }
+
+    if (!psFitsWriteTable (file->fits, header, model, "FP")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        psFree (header);
+        psFree (region);
+        return false;
+    }
+
+    psFree (model);
+    psFree (header);
+    psFree (region);
+    return true;
+}
+
+// second layer is the tangent plane
+bool pmAstromModelWriteTP (pmFPAfile *file) {
+
+    bool status;
+
+    // get the boresite model parameters.  these track the position of the boresite
+    // as a function of the rotator angle
+    float Xo = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.X0");
+    float Yo = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.Y0");
+    float RX = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.RX");
+    float RY = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.RY");
+    float To = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.T0");
+    float Po = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.BORE.P0");
+
+    // the PosZero is the offset between the reported and actual POSANGLE values
+    float PosZero = psMetadataLookupF32 (&status, file->fpa->concepts, "FPA.POS_ZERO");  /// XXX be consistent with degrees v radians
+    char *refChip = psMetadataLookupStr (&status, file->fpa->concepts, "FPA.REF.CHIP");
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "TANGENT_PLANE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "SKY");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "RECTANGLE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "PROJECTION");
+
+    psArray *model = psArrayAllocEmpty (1);
+    psMetadata *row = psMetadataAlloc ();
+    psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "TANGENT_PLANE");
+    psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "SKY");
+
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", region->x0);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", region->x1);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", region->y0);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", region->y1);
+
+    psMetadataAddF32(row,    PS_LIST_TAIL, "XSCALE",   PS_META_REPLACE, "", file->fpa->toSky->Xs * PS_DEG_RAD);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "YSCALE",   PS_META_REPLACE, "", file->fpa->toSky->Ys * PS_DEG_RAD);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_X0",  PS_META_REPLACE, "boresite parameter", Xo);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_Y0",  PS_META_REPLACE, "boresite parameter", Yo);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_RX",  PS_META_REPLACE, "boresite parameter", RX);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_RY",  PS_META_REPLACE, "boresite parameter", RY);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_T0",  PS_META_REPLACE, "boresite parameter", To);
+    psMetadataAddF32(row,    PS_LIST_TAIL, "BORE_P0",  PS_META_REPLACE, "boresite parameter", Po);
+
+    psMetadataAddF32(row,    PS_LIST_TAIL, "POS_ZERO", PS_META_REPLACE, "POSANGLE offset (degrees)", PosZero);
+    psMetadataAddStr(row,    PS_LIST_TAIL, "REF_CHIP", PS_META_REPLACE, "reference chip for model", refChip);
+
+    psArrayAdd (model, 100, row);
+    psFree (row);
+
+    if (!psFitsWriteTable (file->fits, header, model, "TP")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree (region);
+        psFree (model);
+        psFree (header);
+        return false;
+    }
+
+    psFree (region);
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// first layer is the sky
+bool pmAstromModelWriteSky (pmFPAfile *file) {
+
+    psMetadata *header = psMetadataAlloc();
+    psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",   "SKY");
+    psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "NONE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",      "NONE");
+    psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent",    "NONE");
+
+    psArray *model = psArrayAllocEmpty (1);
+    psMetadata *row = psMetadataAlloc ();
+    psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", "SKY");
+    psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "NONE");
+
+    psArrayAdd (model, 100, row);
+    psFree (row);
+
+    if (!psFitsWriteTable (file->fits, header, model, "SKY")) {
+        psError(PS_ERR_IO, false, "writing sky data\n");
+        psFree(model);
+        return false;
+    }
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+/********************* Read Data functions *****************************/
+
+bool pmAstromModelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config)
+    {
+
+        // write the full model in one pass: require the level to be FPA
+        if (view->chip != -1) {
+            psError(PS_ERR_IO, false, "Astrometry must be read at the FPA level");
+            return false;
+        }
+
+        if (!pmAstromModelReadFPA (file)) {
+            psError(PS_ERR_IO, false, "Failed to read Astrometry for fpa");
+            return false;
+        }
+        return true;
+    }
+
+// read out all chip-level Astrometry data for this FPA
+bool pmAstromModelReadFPA (pmFPAfile *file) {
+
+    if (!pmAstromModelReadPHU (file)) {
+        psError(PS_ERR_IO, false, "Failed to read PHU for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelReadChips (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Astrometry for chips");
+        return false;
+    }
+
+    if (!pmAstromModelReadFP (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    // NOTE : TP must come after FP as it applies the POS, ROT boresite corrections to the
+    // transformation determined in FP
+    if (!pmAstromModelReadTP (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    if (!pmAstromModelReadSky (file)) {
+        psError(PS_ERR_IO, false, "Failed to read Sky for Astrometry model");
+        return false;
+    }
+
+    return true;
+}
+
+bool pmAstromModelReadPHU (pmFPAfile *file) {
+
+    // not necessary to read the PHU
+    return true;
+}
+
+int pmConceptsChipNumberFromName (pmFPA *fpa, char *name) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        char *thisone = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+        if (!thisone) continue;
+        if (!strcmp (name, thisone)) return (i);
+    }
+    return -1;
+}
+
+pmChip *pmConceptsChipFromName (pmFPA *fpa, char *name) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        char *thisone = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+        if (!thisone) continue;
+        if (!strcmp (name, thisone)) return (chip);
+    }
+    return NULL;
+}
+
+// first layer converts Chip to Focal Plane
+bool pmAstromModelReadChips (pmFPAfile *file) {
+
+    bool status;
+
+    // set FITS cursor
+    if (!psFitsMoveExtName (file->fits, "CHIPS")) {
+        psError(PS_ERR_IO, false, "missing CHIPS extension in astrometry model\n");
+        return false;
+    }
+
+    // free exising tranformations in prep for new alloc below
+    for (int i = 0; i < file->fpa->chips->n; i++) {
+        pmChip *chip = file->fpa->chips->data[i];
+        psFree (chip->toFPA);
+        chip->toFPA = NULL;
+    }
+
+    // load the header
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    // load the full model in one shot
+    psArray *model = psFitsReadTable (file->fits);
+    if (!model) psAbort("cannot read model");
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from FP\n", model->n);
+
+    // parse the model entries
+    for (int i = 0; i < model->n; i++) {
+        psMetadata *row = model->data[i];
+
+        // name of the chip for this row.
+        char *chipname = psMetadataLookupStr (&status, row, "SEGMENT");
+
+        // get chip from name
+        pmChip *chip = pmConceptsChipFromName (file->fpa, chipname);
+        REQUIRE (chip, "invalid chip name");
+
+        // define the toFPA transform if not already defined
+        int nX = psMetadataLookupS32(&status, row, "NXORDER"); REQUIRE (status, "missing NXORDER");
+        int nY = psMetadataLookupS32(&status, row, "NYORDER"); REQUIRE (status, "missing NYORDER");
+        if (chip->toFPA == NULL) {
+            chip->toFPA = psPlaneTransformAlloc(nX, nY);
+        } else {
+            REQUIRE (chip->toFPA->x->nX == nX, "mismatch in chip order");
+            REQUIRE (chip->toFPA->x->nY == nY, "mismatch in chip order");
+            REQUIRE (chip->toFPA->y->nX == nX, "mismatch in chip order");
+            REQUIRE (chip->toFPA->y->nY == nY, "mismatch in chip order");
+        }
+
+        int ix = psMetadataLookupS32(&status, row, "XORDER");  REQUIRE (status, "missing XORDER");
+        int iy = psMetadataLookupS32(&status, row, "YORDER");  REQUIRE (status, "missing YORDER");
+
+        chip->toFPA->x->coeff[ix][iy]    = psMetadataLookupF32(&status, row, "POLY_X");
+        chip->toFPA->y->coeff[ix][iy]    = psMetadataLookupF32(&status, row, "POLY_Y");
+        chip->toFPA->x->coeffErr[ix][iy] = psMetadataLookupF32(&status, row, "ERROR_X");
+        chip->toFPA->y->coeffErr[ix][iy] = psMetadataLookupF32(&status, row, "ERROR_Y");
+        chip->toFPA->x->coeffMask[ix][iy] = psMetadataLookupU8(&status, row, "MASK_X");
+        chip->toFPA->y->coeffMask[ix][iy] = psMetadataLookupU8(&status, row, "MASK_Y");
+    }
+
+    // convert the toFPA transfomations to fromFPA transformations
+    for (int i = 0; i < file->fpa->chips->n; i++) {
+        pmChip *chip = file->fpa->chips->data[i];
+        if (!chip->toFPA) continue;
+        psRegion *region = pmChipPixels (chip);
+        psFree (chip->fromFPA);
+        chip->fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+        psFree (region);
+    }
+
+    psFree (model);
+    psFree (header);
+    return true;
+}
+
+// second layer converts Focal Plane to Tangent Plane (unrotated)
+bool pmAstromModelReadFP (pmFPAfile *file) {
+
+    bool status;
+
+    if (!psFitsMoveExtName (file->fits, "FP")) {
+        psError(PS_ERR_IO, false, "missing FP extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    // free the old
+    psFree (file->fpa->toTPA);
+    file->fpa->toTPA = NULL;
+
+    // read the complete model data at one shot
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from FP\n", model->n);
+
+    // parse the model
+    for (int i = 0; i < model->n; i++) {
+        psMetadata *row = model->data[i];
+
+        // there is only one transformation in this model; the order is defined in the header
+        int nX = psMetadataLookupS32(&status, row, "NXORDER"); REQUIRE (status, "missing NXORDER");
+        int nY = psMetadataLookupS32(&status, row, "NYORDER"); REQUIRE (status, "missing NYORDER");
+        if (file->fpa->toTPA == NULL) {
+            // allocate the new transformation
+            file->fpa->toTPA = psPlaneTransformAlloc(nX, nY);
+        } else {
+            REQUIRE (file->fpa->toTPA->x->nX == nX, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->x->nY == nY, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->y->nX == nX, "mismatch in chip order");
+            REQUIRE (file->fpa->toTPA->y->nY == nY, "mismatch in chip order");
+        }
+
+        int ix = psMetadataLookupS32(&status, row, "XORDER"); REQUIRE (status, "missing XORDER");
+        int iy = psMetadataLookupS32(&status, row, "YORDER"); REQUIRE (status, "missing YORDER");
+        file->fpa->toTPA->x->coeff[ix][iy]     = psMetadataLookupF32(&status, row, "POLY_X");  REQUIRE (status, "missing POLY_X");
+        file->fpa->toTPA->y->coeff[ix][iy]     = psMetadataLookupF32(&status, row, "POLY_Y");  REQUIRE (status, "missing POLY_Y");
+        file->fpa->toTPA->x->coeffErr[ix][iy]  = psMetadataLookupF32(&status, row, "ERROR_X"); REQUIRE (status, "missing ERROR_X");
+        file->fpa->toTPA->y->coeffErr[ix][iy]  = psMetadataLookupF32(&status, row, "ERROR_Y"); REQUIRE (status, "missing ERROR_Y");
+        file->fpa->toTPA->x->coeffMask[ix][iy] = psMetadataLookupU8 (&status, row, "MASK_X");  REQUIRE (status, "missing MASK_X");
+        file->fpa->toTPA->y->coeffMask[ix][iy] = psMetadataLookupU8 (&status, row, "MASK_Y");  REQUIRE (status, "missing MASK_Y");
+    }
+
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+    psFree (file->fpa->fromTPA);
+    file->fpa->fromTPA = psPlaneTransformInvert(NULL, file->fpa->toTPA, *region, 50);
+
+    psFree (model);
+    psFree (header);
+    psFree (region);
+    return true;
+}
+
+# define TRANSFER(TO,FROM,NAME) { \
+    psMetadataItem *item = psMetadataLookup(FROM,NAME); \
+    if (!item) psAbort ("cannot find %s", NAME); \
+    psMetadataItem *newItem = psMetadataItemCopy(item); \
+    if (!psMetadataAddItem(TO, newItem, PS_LIST_TAIL, PS_META_REPLACE)) { \
+        psAbort ("cannot copy %s", NAME); \
+    } \
+    psFree (newItem); }
+
+// third layer applies boresite corrections and converts tangent plane to sky
+bool pmAstromModelReadTP (pmFPAfile *file) {
+
+    if (!psFitsMoveExtName (file->fits, "TP")) {
+        psError(PS_ERR_IO, false, "missing TP extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from TP\n", model->n);
+    if (model->n != 1) psAbort("invalid number of rows in TP model (%ld)", model->n);
+
+    psMetadata *row = model->data[0];
+
+    // move needed items to the concepts
+    TRANSFER (file->fpa->concepts, row, "XSCALE");
+    TRANSFER (file->fpa->concepts, row, "XSCALE");
+    TRANSFER (file->fpa->concepts, row, "YSCALE");
+    TRANSFER (file->fpa->concepts, row, "BORE_X0");
+    TRANSFER (file->fpa->concepts, row, "BORE_Y0");
+    TRANSFER (file->fpa->concepts, row, "BORE_RX");
+    TRANSFER (file->fpa->concepts, row, "BORE_RY");
+    TRANSFER (file->fpa->concepts, row, "BORE_T0");
+    TRANSFER (file->fpa->concepts, row, "BORE_P0");
+    TRANSFER (file->fpa->concepts, row, "POS_ZERO");
+    TRANSFER (file->fpa->concepts, row, "REF_CHIP");
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// first layer is the sky
+bool pmAstromModelReadSky (pmFPAfile *file) {
+
+    if (!psFitsMoveExtName (file->fits, "SKY")) {
+        psError(PS_ERR_IO, false, "missing SKY extension in astrometry model\n");
+        return false;
+    }
+
+    psMetadata *header = psFitsReadHeader(NULL, file->fits); // The FITS header
+    if (!header) psAbort("cannot read model header");
+
+    psArray *model = psFitsReadTable (file->fits);
+    psLogMsg ("psModules.astrom", 4, "read %ld rows from SKY\n", model->n);
+    if (model->n != 1) psAbort("invalid number of rows in SKY model (%ld)", model->n);
+
+    // XXX not much information of interest in this table...
+
+    // generate a template projection for comparisons
+    psFree (file->fpa->toSky);
+    file->fpa->toSky = psProjectionAlloc (0.0, 0.0, PS_RAD_DEG/3600.0, PS_RAD_DEG/3600.0, PS_PROJ_DIS);
+
+    psFree (model);
+    psFree (header);
+    return (true);
+}
+
+// third layer applies boresite corrections and converts tangent plane to sky
+bool pmAstromModelSetTP (pmFPAfile *file, psMetadata *concepts) {
+
+    bool status;
+
+    // these externally supplied values are used to set the final transformation terms
+    double RA  = psMetadataLookupF64 (&status, concepts, "FPA.RA"); REQUIRE (status, "missing FPA.RA");
+    double DEC = psMetadataLookupF64 (&status, concepts, "FPA.DEC"); REQUIRE (status, "missing FPA.DEC");
+    double POS = PM_RAD_DEG * psMetadataLookupF64 (&status, concepts, "FPA.POSANGLE"); REQUIRE (status, "missing FPA.POSANGLE");
+
+    // get projection scale; center is supplied
+    float Xs = psMetadataLookupF32(&status, file->fpa->concepts, "XSCALE") * PM_RAD_DEG; REQUIRE (status, "missing XSCALE");
+    float Ys = psMetadataLookupF32(&status, file->fpa->concepts, "YSCALE") * PM_RAD_DEG; REQUIRE (status, "missing YSCALE");
+
+    // allocate a new toSky projection using the reported position
+    psFree (file->fpa->toSky);
+    file->fpa->toSky = psProjectionAlloc (RA, DEC, Xs, Ys, PS_PROJ_DIS);
+
+    // get boresite correction terms.  RX,RY,To,Po define an ellipse along which the boresite travels
+    double Xo = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_X0"); REQUIRE (status, "missing ");
+    double Yo = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_Y0"); REQUIRE (status, "missing ");
+    double RX = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_RX"); REQUIRE (status, "missing ");
+    double RY = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_RY"); REQUIRE (status, "missing ");
+    double To = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_T0"); REQUIRE (status, "missing ");
+    double Po = psMetadataLookupF32(&status, file->fpa->concepts, "BORE_P0"); REQUIRE (status, "missing ");
+
+    // the true rotation of the instrument is POSANGLE - POS_ZERO
+    double PosZero = PM_RAD_DEG * psMetadataLookupF32(&status, file->fpa->concepts, "POS_ZERO"); REQUIRE (status, "missing POS_ZERO");
+    char *refChip  = psMetadataLookupStr(&status, file->fpa->concepts, "REF_CHIP"); REQUIRE (status, "missing REF_CHIP");
+
+    // XXX we've swapped the sign and parity of POS (add to model)
+    // apply true posangle = -(POS - POS_ZERO)
+    psLogMsg ("psModules.astrom", 4, "Position Angle: %f, Model Position Angle Zero Point: %f\n", POS, PosZero);
+    psPlaneTransform *fromTPA = psPlaneTransformRotate (NULL, file->fpa->fromTPA, (PosZero - POS));
+    psFree (file->fpa->fromTPA);
+    file->fpa->fromTPA = fromTPA;
+
+    psRegion *region = pmAstromFPAExtent (file->fpa);
+    psFree (file->fpa->toTPA);
+    file->fpa->toTPA = psPlaneTransformInvert(NULL, file->fpa->fromTPA, *region, 50);
+    psFree (region);
+
+    // current position of the nominal boresite in refChip coordinates
+    double X = Xo + RX*cos(POS - To)*cos(Po) + RY*sin(POS - To)*sin(Po);
+    double Y = Yo + RY*sin(POS - To)*cos(Po) - RX*cos(POS - To)*sin(Po);
+    psLogMsg ("psModules.astrom", 4, "Boresite coords on reference chip: %f, %f\n", X, Y);
+
+    // get reference chip from name
+    pmChip *chip = pmConceptsChipFromName (file->fpa, refChip);
+    if (!chip) psAbort ("invalid chip name for reference");
+
+    psPlane *boreCH = psPlaneAlloc();
+    psPlane *boreFP = psPlaneAlloc();
+    psPlane *boreTP = psPlaneAlloc();
+    psSphere *boreSky = psSphereAlloc();
+
+    // find the FP coord of the reported boresite location
+    boreCH->x = X;
+    boreCH->y = Y;
+    psPlaneTransformApply (boreFP, chip->toFPA, boreCH);
+
+    // find the true RA,DEC coord of the mirror of the reported boresite FP location
+    boreFP->x = -boreFP->x;
+    boreFP->y = -boreFP->y;
+    psPlaneTransformApply (boreTP, file->fpa->toTPA, boreFP);
+    psDeproject (boreSky, boreTP, file->fpa->toSky);
+
+    // modify the projection to account for offset between true and reported boresite
+    file->fpa->toSky->R = boreSky->r;
+    file->fpa->toSky->D = boreSky->d;
+
+    psTrace ("psModules.astrom", 5, "actual boresite coordinates: %lf, %lf\n", file->fpa->toSky->R*PS_DEG_RAD, file->fpa->toSky->D*PS_DEG_RAD);
+    psTrace ("psModules.astrom", 5, "plate scale used: %lf, %lf\n", file->fpa->toSky->Xs*PS_DEG_RAD*3600.0, file->fpa->toSky->Ys*PS_DEG_RAD*3600.0);
+
+    psFree (boreCH);
+    psFree (boreFP);
+    psFree (boreTP);
+    psFree (boreSky);
+
+    return (true);
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryModel.h	(revision 22306)
@@ -0,0 +1,43 @@
+/* @file  pmAstrometryModel.h
+ * @brief Astrometry model I/O functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_MODEL_H
+#define PM_ASTROMETRY_MODEL_H
+
+/// @addtogroup Astrometry
+/// @{
+
+bool pmAstromModelCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file);
+bool pmAstromModelCheckDataStatusForFPA (const pmFPA *fpa);
+bool pmAstromModelCheckDataStatusForChip (const pmChip *chip);
+
+bool pmAstromModelWriteForView (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+bool pmAstromModelWriteFPA (pmFPAfile *file, const pmFPA *fpa);
+bool pmAstromModelWritePHU (pmFPAfile *file, const pmFPA *fpa);
+bool pmAstromModelWriteSky (pmFPAfile *file);
+bool pmAstromModelWriteTP (pmFPAfile *file);
+bool pmAstromModelWriteFP (pmFPAfile *file);
+bool pmAstromModelWriteChips (pmFPAfile *file);
+
+int pmConceptsChipNumberFromName (pmFPA *fpa, char *name);
+pmChip *pmConceptsChipFromName (pmFPA *fpa, char *name);
+
+bool pmAstromModelReadForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromModelReadFPA (pmFPAfile *file);
+bool pmAstromModelReadPHU (pmFPAfile *file);
+bool pmAstromModelReadChips (pmFPAfile *file);
+bool pmAstromModelReadFP (pmFPAfile *file);
+bool pmAstromModelReadTP (pmFPAfile *file);
+bool pmAstromModelReadSky (pmFPAfile *file);
+
+bool pmAstromModelSetTP (pmFPAfile *file, psMetadata *concepts);
+
+/// @}
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.c	(revision 22306)
@@ -0,0 +1,929 @@
+/** @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.39 $ $Name: not supported by cvs2svn $
+*  @date $Date: 2008-04-11 07:42:05 $
+*
+*  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 "pmAstrometryObjects.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_U8);
+    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;
+
+            // fprintf (f, "dX,dY: %8.2f %8.2f : %8.2f %8.2f : %8.2f %8.2f\n", dX, dY, ob1->FP->x, ob2->FP->x, 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];
+        }
+
+        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);
+    psFree(xHist);
+    psFree(yHist);
+    xHist = xHistNew;
+    yHist = yHistNew;
+
+    // select peak in x and in y
+    xBin = yBin = 0;
+    double xMax = 0;
+    double yMax = 0;
+    for (int i = 0; i < nBin; i++) {
+        if (xHist->data.F32[i] > xMax) {
+            xBin = i;
+            xMax = xHist->data.F32[i];
+        }
+        if (yHist->data.F32[i] > yMax) {
+            yBin = i;
+            yMax = yHist->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;
+
+    psFree (rot);
+    psFree (xHist);
+    psFree (yHist);
+
+    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: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryObjects.h	(revision 22306)
@@ -0,0 +1,348 @@
+/* @file  pmAstrometryObjects.h
+ * @brief basic matching of objects based on their astrometry.
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.17 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-11-21 07:02:55 $
+ * Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_OBJECTS_H
+#define PM_ASTROMETRY_OBJECTS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+/*
+ *
+ * This structure specifies the coordinate of the detection in each of the
+ * four necessary coordinate frames: pix defines the position in the psReadout
+ * frame, FP defines the position in the Focal Plane frame, TP defines the
+ * position in the Tangent Plane frame, sky defines the position on the Celestial
+ * Sphere. In addition, a measurement of the brightness is given by the element
+ * Mag. Such a data structure should be used for both the raw and the reference
+ * stars. In astrometric processing, the raw detections will be projected using
+ * the best available information to each of these coordinate frames from the pix
+ * coordinates, while the reference detections will be projected to the other
+ * frames from the sky coordinates.
+ *
+ * XXX: There are more members here than in the SDRS.
+ *
+ */
+typedef struct
+{
+    psPlane *pix;   ///< the position in the pmReadout frame
+    psPlane *cell;   ///< the position in the pmCell frame
+    psPlane *chip;   ///< the position in the pmChip frame
+    psPlane *FP;   ///< the position in the pmFPA frame
+    psPlane *TP;   ///< the position in the tangent plane
+    psSphere *sky;        ///< the position on the Celestial Sphere.
+    double Mag;                         ///< object magnitude XXX what filter?
+    double dMag;                        ///< error on object magnitude
+}
+pmAstromObj;
+
+/*
+ *
+ * The pmAstromMatch structure defines the cross-correlation between two
+ * arrays. A single such data item specifies that item number pmAstromMatch.idx1
+ * in the first list corresponds to pmAstromMatch.idx2 in the second list.
+ *
+ */
+typedef struct
+{
+    int raw;                             ///< What is this?
+    int ref;                             ///< What is this?
+}
+pmAstromMatch;
+
+
+/*
+ *
+ * XXX: Not in SDRS.
+ *
+ */
+typedef struct
+{
+    psPlane center;                     ///<
+    psPlane offset;                     ///<
+    double  scale;                      ///<
+    double  angle;                      ///<
+    double  minMetric;                  ///<
+    double  minVar;                     ///<
+    int     nMatch;                     ///<
+    int     nTest;                      ///<
+    double  nSigma;                     ///<
+}
+pmAstromStats;
+
+typedef struct
+{
+    psStats *xStats;
+    psStats *yStats;
+    int     nMatch;                     ///<
+    double  nSigma;                     ///<
+}
+pmAstromFitResults;
+
+/*
+ *
+ * If the two sets of coordinates are expected to agree very well (ie, the current best-guess
+ * astrometric solution is quite close to reality), perform a match based on a simple radius
+ * test. The following functions accept two sets of pmAstromObj sources and determines the
+ * matched objects between the two lists using coordinates of the desired depth (depending on
+ * the function). The input and reference sources must have been projected to the desired depth
+ * (eg, for Focal Plane coordinates, to pmAstromObj.FP).  The specified radius must be in the
+ * units for the matching depth (chip: pixels, focal plane: microns, tangent plane:
+ * degrees. The output consists an array of pmAstromMatch values, defined above.
+ *
+ */
+psArray *pmAstromRadiusMatch(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchFP(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchTP(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+psArray *pmAstromRadiusMatchChip(
+    const psArray *st1,
+    const psArray *st2,
+    double RADIUS
+);
+
+
+pmAstromStats *pmAstromStatsAlloc(void);
+
+/*
+ *
+ * This function accepts an array of pmAstromObj objects and rotates them by
+ * the given angle about the given center coordinate pCenter,qCenter in the Focal
+ * Plane Array coordinates.
+ *
+ * XXX: This differs from the SDRS
+ *
+ */
+/* SDRS
+psArray *pmAstromRotateObj(
+    psArray *old,
+    double angle,
+    double pCenter,
+    double qCenter
+);
+*/
+psArray *pmAstromRotateObj(
+    const psArray *old,
+    psPlane center,
+    double angle,
+    double scale
+);
+
+
+/*
+ *
+ * If the two sets of coordinates are not known to agree well, but the
+ * relative scale and approximate relative rotation is known, then a much faster
+ * match can be found using pair-pair displacements. In such a case, the two
+ * lists can be considered as having the same coordinate system, with an unknown
+ * relative displacement. In this algorithm, all possible pair-wise differences
+ * between the source positions in the two lists are constructed and accumulated
+ * in a grid of possible offset values. The resulting grid is searched for a
+ * cluster representing the offset between the two input lists. This algorithm
+ * can only tolerate a small error in the relative scale or the relative rotation
+ * of the two coordinate lists. However, this process is naturally O(N2), and is
+ * thus advantageous over triangle matching in some circumstances. This process
+ * can be extended to allow a larger uncertainty in the relative rotation by
+ * allowing the procedure to scan over a range of rotations. We define the
+ * following function to apply this matching algorithm:
+ *
+ * XXX: In the SDRS, this function is a pointer.
+ *
+ */
+pmAstromStats *pmAstromGridMatch(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config
+);
+
+/******************************************************************************
+pmAstromGridTweak(*raw, *ref, *recipe, stats): improve match for two star lists.
+ ******************************************************************************/
+pmAstromStats *pmAstromGridTweak(
+    psArray *raw,
+    psArray *ref,
+    psMetadata *recipe,
+    pmAstromStats *stats);
+
+/*
+ *
+ * The result of a pmAstromGridMatch may be used to modify the astrometry
+ * transformation information for a pmFPA image hierarchy structure. The result
+ * of pmAstromGridMatch defines the adjustments which should be made to the
+ * reference coordinate of the projection (pmFPA.projection.R,D) and the
+ * effective rotation of the Focal Plane.  The rotation implies modification of
+ * the linear terms of the pmFPA.toTangentPlane transformation. These two
+ * adjustments are made using the function:
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+psPlaneTransform *pmAstromGridApply(
+    psPlaneTransform *map,
+    pmAstromStats *stat
+);
+
+
+/*
+ *
+ * This function is identical to pmAstromGridMatch, but is valid for only a
+ * single relative rotation. The input config information need not contain any of
+ * the GRID.*.ANGLE entries (they will be ignored).
+ *
+ * XXX: This function name is different in the SDRS.
+ *
+ */
+/* in pmAstromGrid.c */
+pmAstromStats *pmAstromGridAngle(
+    const psArray *st1,
+    const psArray *st2,
+    const psMetadata *config);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists and the list of
+ * matched entries. It uses the matched list to determine a polynomial
+ * transformation between the two coordinate systems. The fitting uses clipping
+ * to exclude outliers, likely representing poor matches. The config element must
+ * contain the information ASTROM.NSIGMA (specifying the number of sigma used in
+ * the clipping) and ASTROM.NCLIP (specifying the number of clipping iterations
+ * must be performed). The config element must also specify the order of the
+ * polynomial fit (keyword: ASTROM.ORDER). The result of this fit is a set of
+ * modifications of the components of the pmFPA.toTangentPlane transformation,
+ * and the modifications of the reference coordinate of the projection
+ * (pmFPA.projection.R,D) and the projection scale (pmFPA.projection.Xs,Ys). The
+ * modifications to pmFPA.toTangentPlane incorporate the rotation component of
+ * the linear terms and the higher-order terms of the polynomial fits.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitFPA(
+    pmFPA *fpa,
+    psArray *st1,
+    psArray *st2,
+    psArray *match,
+    psMetadata *config
+);
+
+
+
+/*
+ *
+ * This function accepts the raw and reference source lists for a single chip
+ * and the list of matched entries. It uses the matched list to determine a
+ * polynomial transformation between the two coordinate systems. The fitting
+ * uses clipping to exclude outliers, likely representing poor matches. The
+ * config element must contain the information ASTROM.NSIGMA
+ *(specifying the number of sigma used in the clipping) and ASTROM.NCLIP
+ *(specifying the number of clipping iterations must be performed). The config
+ *element must also specify the order of the polynomial fit (keyword:
+ *ASTROM.ORDER).  The result of this fit is a set of modifications of the
+ *components of the pmChip.toFPA transformation.
+ *
+ * XXX: No prototype code.
+ *
+ */
+bool pmAstromFitChip(
+    pmFPA *fpa,
+    psArray *st1,
+    psArray *st2,
+    psArray *match,
+    psMetadata *config
+);
+
+
+/*******************************************************************************
+ The following functions and structs were in the prototype code, but not the
+ SDRS.
+ ******************************************************************************/
+/*
+ *
+ *
+ *
+ *
+ */
+
+pmAstromFitResults *pmAstromFitResultsAlloc(void);
+
+/*
+ *
+ * Allocates a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjAlloc (void);
+/*
+ * Is a given pointer a pmAstromObj?
+ */
+bool pmAstromObjTest(const psPtr ptr);
+
+
+/*
+ *
+ * Copies a pmAstromObj struct.
+ *
+ */
+pmAstromObj *pmAstromObjCopy(
+    const pmAstromObj *old
+);
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromMatch *pmAstromMatchAlloc(
+    int i1,
+    int i2
+);
+
+
+
+
+/*
+ *
+ *
+ *
+ */
+pmAstromFitResults *pmAstromMatchFit(
+    psPlaneTransform *map,
+    psArray *raw,
+    psArray *ref,
+    psArray *match,
+    psStats *stats
+);
+
+/*
+ *
+ *
+ *
+ */
+int pmAstromObjSortByMag(
+    const void **a,
+    const void **b
+);
+
+/// @}
+#endif // PM_ASTROMETRY_OBJECTS_H
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.c	(revision 22306)
@@ -0,0 +1,299 @@
+/* @file  pmAstrometryRefstars.c
+ * @brief Functions to write (and read?) astrometric reference stars
+ *
+ * @ingroup AstroImage
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.3 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2008 Institute for Astronomy, 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 "pmConfig.h"
+#include "pmDetrendDB.h"
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPALevel.h"
+#include "pmFPAview.h"
+#include "pmFPAfile.h"
+#include "pmFPAfileFitsIO.h"
+#include "pmAstrometryObjects.h"
+#include "pmAstrometryRefstars.h"
+
+/********************* CheckDataStatus functions *****************************/
+
+bool pmAstromRefstarsCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file) {
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        bool exists = pmAstromRefstarsCheckDataStatusForFPA (fpa);
+        return exists;
+    }
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_IO, true, "Requested chip == %d >= fpa->chips->n == %ld", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        bool exists = pmAstromRefstarsCheckDataStatusForChip (chip);
+        return exists;
+    }
+    if (view->cell >= chip->cells->n) {
+        psError(PS_ERR_IO, true, "Requested cell == %d >= chip->cells->n == %ld", view->cell, chip->cells->n);
+        return false;
+    }
+    psError(PS_ERR_IO, false, "Astrometry only valid at the chip level");
+    return false;
+}
+
+// return true if data exists for any chip
+bool pmAstromRefstarsCheckDataStatusForFPA (const pmFPA *fpa) {
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        if (!chip) continue;
+        if (pmAstromRefstarsCheckDataStatusForChip (chip)) return true;
+    }
+    return false;
+}
+
+// return true if data exists for any cell
+bool pmAstromRefstarsCheckDataStatusForChip (const pmChip *chip) {
+
+    for (int i = 0; i < chip->cells->n; i++) {
+      pmCell *cell = chip->cells->data[i];
+        if (!cell) continue;
+        if (pmAstromRefstarsCheckDataStatusForCell (cell)) return true;
+    }
+    return false;
+}
+
+// return true if data exists for any readout
+bool pmAstromRefstarsCheckDataStatusForCell (const pmCell *cell) {
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+      pmReadout *readout = cell->readouts->data[i];
+        if (!readout) continue;
+        if (pmAstromRefstarsCheckDataStatusForReadout (readout)) return true;
+    }
+    return false;
+}
+
+// check if refstars array exists
+bool pmAstromRefstarsCheckDataStatusForReadout (const pmReadout *readout) {
+
+  if (!readout->analysis) return false;
+
+  // select the raw objects for this readout
+  psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+  if (refstars == NULL) return false;
+
+  return true;
+}
+
+/********************* Write Data functions *****************************/
+
+bool pmAstromRefstarsWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+    PS_ASSERT_PTR_NON_NULL(file->fpa, false);
+
+    pmFPA *fpa = file->fpa;
+
+    if (view->chip == -1) {
+        if (!pmAstromRefstarsWriteFPA (fpa, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write refstars for fpa");
+            return false;
+        }
+        return true;
+    }
+
+    if (view->chip >= fpa->chips->n) {
+        psError(PS_ERR_UNKNOWN, false, "Writing chip == %d (>= chips->n == %ld)", view->chip, fpa->chips->n);
+        return false;
+    }
+    pmChip *chip = fpa->chips->data[view->chip];
+
+    if (view->cell == -1) {
+        if (!pmAstromRefstarsWriteChip (chip, view, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write refstars for chip");
+            return false;
+        }
+        return true;
+    }
+
+    psError(PS_ERR_IO, false, "Astrometry must be written at the FPA level");
+    return false;
+}
+
+bool pmAstromRefstarsWritePHU (const pmFPAview *view, pmFPAfile *file, pmConfig *config) {
+
+    // output header data
+    psMetadata *outhead = psMetadataAlloc();
+
+    // use the FPA phu to generate the PHU header
+    pmFPA *fpa = pmFPAfileSuitableFPA(file, view, config, false); // Suitable FPA for writing
+    pmHDU *phu = psMemIncrRefCounter(fpa->hdu);
+    psFree(fpa);
+
+    // if there is no FPA PHU, this is a single header+image (extension-less) file. This could be
+    // the case for an input SPLIT set of files being written out as a MEF.  if there is a PHU,
+    // write it out as a 'blank'
+    if (phu) {
+        psMetadataCopy (outhead, phu->header);
+    } else {
+        pmConfigConformHeader (outhead, file->format);
+    }
+    psFree(phu);
+
+    psMetadataAddBool (outhead, PS_LIST_TAIL, "EXTEND", PS_META_REPLACE, "this file has extensions", true);
+    psFitsWriteBlank (file->fits, outhead, "");
+    file->wrote_phu = true;
+
+    psTrace ("pmFPAfile", 5, "wrote phu %s (type: %d)\n", file->filename, file->type);
+    psFree (outhead);
+
+    return true;
+}
+
+// write out all chip-level Astrometry data for this FPA
+bool pmAstromRefstarsWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(fpa, false);
+    PS_ASSERT_PTR_NON_NULL(fpa->chips, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        thisView->chip = i;
+        if (!pmAstromRefstarsWriteChip (chip, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth chip", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+bool pmAstromRefstarsWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, false);
+    PS_ASSERT_PTR_NON_NULL(chip->cells, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < chip->cells->n; i++) {
+        pmCell *cell = chip->cells->data[i];
+        thisView->cell = i;
+        if (!pmAstromRefstarsWriteCell (cell, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth cell", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// read in all readout-level Objects files for this cell
+bool pmAstromRefstarsWriteCell (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(cell, false);
+    PS_ASSERT_PTR_NON_NULL(cell->readouts, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+
+    pmFPAview *thisView = pmFPAviewAlloc (view->nRows);
+    *thisView = *view;
+
+    for (int i = 0; i < cell->readouts->n; i++) {
+        pmReadout *readout = cell->readouts->data[i];
+        thisView->readout = i;
+        if (!pmAstromRefstarsWriteReadout (readout, thisView, file, config)) {
+            psError(PS_ERR_IO, false, "Failed to write %dth readout", i);
+            psFree (thisView);
+            return false;
+        }
+    }
+    psFree (thisView);
+    return true;
+}
+
+// write out all refstars files for this readout
+bool pmAstromRefstarsWriteReadout (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config) {
+
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(readout->analysis, false);
+    PS_ASSERT_PTR_NON_NULL(view, false);
+    PS_ASSERT_PTR_NON_NULL(file, false);
+
+    // select the raw objects for this readout
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+    if (refstars == NULL) { return TRUE; }
+
+    psMetadata *header = psMetadataAlloc();
+
+    psArray *table = psArrayAllocEmpty (1);
+
+    // set the extname : we are really only allowed one entry per chip; check this here?
+    char *chiprule = psStringCopy ("{CHIP.NAME}");
+    char *chipname = pmFPAfileNameFromRule (chiprule, file, view);
+
+    for (int i = 0; i < refstars->n; i++) {
+      psMetadata *row = psMetadataAlloc ();
+
+      pmAstromObj *ref = refstars->data[i];
+
+      psMetadataAddF64(row,    PS_LIST_TAIL, "RA",      PS_META_REPLACE, "degrees", PS_DEG_RAD*ref->sky->r);
+      psMetadataAddF64(row,    PS_LIST_TAIL, "DEC",     PS_META_REPLACE, "degrees", PS_DEG_RAD*ref->sky->d);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "TP_X",    PS_META_REPLACE, "microns", ref->TP->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "TP_Y",    PS_META_REPLACE, "microns", ref->TP->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "FP_X",    PS_META_REPLACE, "microns", ref->FP->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "FP_Y",    PS_META_REPLACE, "microns", ref->FP->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "CHIP_X",  PS_META_REPLACE, "microns", ref->chip->x);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "CHIP_Y",  PS_META_REPLACE, "microns", ref->chip->y);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "MAG",     PS_META_REPLACE, "microns", ref->Mag);
+      psMetadataAddF32(row,    PS_LIST_TAIL, "MAG_ERR", PS_META_REPLACE, "microns", ref->dMag);
+
+      psArrayAdd (table, 100, row);
+      psFree (row);
+    }
+    if (!psFitsWriteTable (file->fits, header, table, chipname)) {
+        psError(PS_ERR_IO, false, "writing refstars\n");
+        psFree (table);
+        psFree (header);
+        psFree (chiprule);
+        psFree (chipname);
+        return false;
+    }
+
+    psFree (chiprule);
+    psFree (chipname);
+    psFree (table);
+    psFree (header);
+    return true;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRefstars.h	(revision 22306)
@@ -0,0 +1,34 @@
+/* @file  pmAstrometryRefstars.h
+ * @brief Functions to write (and read?) astrometric reference stars
+ *
+ * @ingroup AstroImage
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.2 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2008-07-17 22:38:15 $
+ * Copyright 2008 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_REFSTARS_H
+#define PM_ASTROMETRY_REFSTARS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+bool pmAstromRefstarsCheckDataStatusForView (const pmFPAview *view, pmFPAfile *file);
+bool pmAstromRefstarsCheckDataStatusForFPA (const pmFPA *fpa);
+bool pmAstromRefstarsCheckDataStatusForChip (const pmChip *chip);
+bool pmAstromRefstarsCheckDataStatusForCell (const pmCell *cell);
+bool pmAstromRefstarsCheckDataStatusForReadout (const pmReadout *readout);
+
+bool pmAstromRefstarsWriteForView (const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWritePHU (const pmFPAview *view, pmFPAfile *file, pmConfig *config);
+
+bool pmAstromRefstarsWriteFPA (pmFPA *fpa, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteChip (pmChip *chip, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteCell (pmCell *cell, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+bool pmAstromRefstarsWriteReadout (pmReadout *readout, const pmFPAview *view, pmFPAfile *file, const pmConfig *config);
+
+
+/// @}
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.c	(revision 22306)
@@ -0,0 +1,170 @@
+/** @file  pmAstrometryRegions.c
+ *  @brief functions to define astrometry regions on FPA images
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *  @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2007-12-22 17:51:48 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmFPAExtent.h"
+#include "pmAstrometryRegions.h"
+
+// cell pixels corresponding to readout boundary
+psRegion *pmAstromReadoutInCell (pmReadout *readout) {
+
+    psRegion *region;
+    region = pmReadoutExtent (readout);
+    return (region);
+}
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromCellInChip (pmCell *cell) {
+
+    psRegion *region;
+    region = pmCellExtent (cell);
+    return (region);
+}
+
+// FP pixels corresponding to chip boundary
+// since the chip may be rotated in the fpa, this region does not correspond 
+// exactly to the pixel grid of the chip
+psRegion *pmAstromChipInFP (pmChip *chip) {
+
+    PS_ASSERT_PTR_NON_NULL(chip, NULL);
+
+    // if we have not astrometry for this chip, just skip it silently (no error)
+    if (!chip->toFPA) return NULL;
+
+    // determine the bounding box of this chip in chip pixels
+    psRegion *chipExtent = pmChipPixels (chip);
+    if (!chipExtent) return NULL;
+
+    // apply chip-to-fpa astrometry to determine fpa coordinates 
+    psPlane *chPix = psPlaneAlloc ();
+    psPlane *fpPix = psPlaneAlloc ();
+    psRegion *chipRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of chip
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y0;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x1;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    chPix->x = chipExtent->x0;
+    chPix->y = chipExtent->y1;
+    psPlaneTransformApply(fpPix, chip->toFPA, chPix); 
+    chipRegion->x0 = PS_MIN (chipRegion->x0, fpPix->x);
+    chipRegion->x1 = PS_MAX (chipRegion->x1, fpPix->x);
+    chipRegion->y0 = PS_MIN (chipRegion->y0, fpPix->y);
+    chipRegion->y1 = PS_MAX (chipRegion->y1, fpPix->y);
+
+    psFree (chPix);
+    psFree (fpPix);
+    psFree (chipExtent);
+    return (chipRegion);
+}
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the mosaic center and is used for astrometric reference.
+psRegion *pmAstromFPAExtent(const pmFPA *fpa)
+{
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+
+    psArray *chips = fpa->chips;       // Array of component chips
+    psRegion *fpaExtent = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+    for (long i = 0; i < chips->n; i++) {
+        pmChip *chip = chips->data[i];  // Chip of interest
+        psRegion *chipExtent = pmAstromChipInFP(chip); // Extent of chip
+	if (!chipExtent) { continue; }
+        fpaExtent->x0 = PS_MIN(fpaExtent->x0, chipExtent->x0);
+        fpaExtent->x1 = PS_MAX(fpaExtent->x1, chipExtent->x1);
+        fpaExtent->y0 = PS_MIN(fpaExtent->y0, chipExtent->y0);
+        fpaExtent->y1 = PS_MAX(fpaExtent->y1, chipExtent->y1);
+        psFree(chipExtent);
+    }
+
+    return fpaExtent;
+}
+
+// TPA pixels corresponding to FPA boundary
+psRegion *pmAstromFPInTP (pmFPA *fpa) {
+
+    PS_ASSERT_PTR_NON_NULL(fpa, NULL);
+    PS_ASSERT_PTR_NON_NULL(fpa->toTPA, NULL);
+
+    psRegion *fpaExtent = pmAstromFPAExtent (fpa);
+    if (!fpaExtent) return NULL;
+
+    // apply fpa-to-tpa astrometry to determine tpa coordinates 
+    psPlane *fpPix = psPlaneAlloc ();
+    psPlane *tpPix = psPlaneAlloc ();
+    psRegion *fpaRegion = psRegionAlloc(INFINITY, 0, INFINITY, 0); // Extent of fpa
+
+    // determine the outer bounding box -- does not correspond to the same square region!
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y0;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x1;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    fpPix->x = fpaExtent->x0;
+    fpPix->y = fpaExtent->y1;
+    psPlaneTransformApply(tpPix, fpa->toTPA, fpPix); 
+    fpaRegion->x0 = PS_MIN (fpaRegion->x0, tpPix->x);
+    fpaRegion->x1 = PS_MAX (fpaRegion->x1, tpPix->x);
+    fpaRegion->y0 = PS_MIN (fpaRegion->y0, tpPix->y);
+    fpaRegion->y1 = PS_MAX (fpaRegion->y1, tpPix->y);
+
+    psFree (fpPix);
+    psFree (tpPix);
+    psFree (fpaExtent);
+    return (fpaRegion);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryRegions.h	(revision 22306)
@@ -0,0 +1,35 @@
+/* @file  pmAstrometryRegion.h
+ * @brief functions to detemine fpa,chip,etc boundaries from astrometry
+ *
+ * @author EAM, IfA
+ * @version $Revision: 1.1 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-21 21:59:57 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_REGIONS_H
+#define PM_ASTROMETRY_REGIONS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+// cell pixels corresponding to readout boundary
+psRegion *pmAstromReadoutInCell (pmReadout *readout);
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromCellInChip (pmCell *cell);
+
+// FP pixels corresponding to chip boundary
+// since the chip may be rotated in the fpa, this region does not correspond 
+// exactly to the pixel grid of the chip
+psRegion *pmAstromChipInFP (pmChip *chip);
+
+// return FPA pixels included in all chips
+// this FPA grid has 0,0 at the mosaic center and is used for astrometric reference.
+psRegion *pmAstromFPAExtent(const pmFPA *fpa);
+
+// chip pixels corresponding to cell boundary
+psRegion *pmAstromFPInTP (pmFPA *fpa);
+
+/// @}
+#endif // PM_ASTROMETRY_DISTORTION_H
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.c	(revision 22306)
@@ -0,0 +1,392 @@
+/** @file  pmAstrometryUtils.c
+ *
+ *  @brief utility functions for transform and distort functions
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.9 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-10-08 20:18:00 $
+ *
+ *  Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pslib.h>
+
+#include "pmHDU.h"
+#include "pmFPA.h"
+#include "pmAstrometryUtils.h"
+
+// given a 2D transformation -- L(x,y),M(x,y) -- find the coordinates x,y
+// for which L,M = 0,0. tol is the allowed error on x,y.
+psPlane *psPlaneTransformGetCenter (psPlaneTransform *trans, double tol)
+{
+
+    // crpix1,2 = X,Y(crval1,2)
+    // start with linear solution for Xo,Yo:
+    double R  = (trans->x->coeff[1][0]*trans->y->coeff[0][1] - trans->x->coeff[0][1]*trans->y->coeff[1][0]);
+    double Xo = (trans->y->coeff[0][0]*trans->x->coeff[0][1] - trans->x->coeff[0][0]*trans->y->coeff[0][1])/R;
+    double Yo = (trans->x->coeff[0][0]*trans->y->coeff[1][0] - trans->y->coeff[0][0]*trans->x->coeff[1][0])/R;
+
+    // iterate to actual solution: requires small non-linear terms
+    if (trans->x->nX > 1) {
+        psPolynomial2D *XdX = psPolynomial2D_dX(NULL, trans->x);
+        psPolynomial2D *XdY = psPolynomial2D_dY(NULL, trans->x);
+
+        psPolynomial2D *YdX = psPolynomial2D_dX(NULL, trans->y);
+        psPolynomial2D *YdY = psPolynomial2D_dY(NULL, trans->y);
+
+        psImage *Alpha = psImageAlloc (2, 2, PS_DATA_F32);
+        psVector *Beta = psVectorAlloc (2, PS_DATA_F32);
+
+        /* this loop uses the Newton-Raphson method to solve for Xo,Yo
+        * it needs the high order terms to be small 
+        * Xo,Yo are in pixels;
+        */
+        double dPos = tol + 1;
+        for (int i = 0; (dPos > tol) && (i < 20); i++) {
+            // NOTE: order for Alpha is: [y][x]
+            Alpha->data.F32[0][0] = psPolynomial2DEval (XdX, Xo, Yo);
+            Alpha->data.F32[1][0] = psPolynomial2DEval (XdY, Xo, Yo);
+            Alpha->data.F32[0][1] = psPolynomial2DEval (YdX, Xo, Yo);
+            Alpha->data.F32[1][1] = psPolynomial2DEval (YdY, Xo, Yo);
+
+            Beta->data.F32[0] = psPolynomial2DEval (trans->x, Xo, Yo);
+            Beta->data.F32[1] = psPolynomial2DEval (trans->y, Xo, Yo);
+
+            if (!psMatrixGJSolveF32 (Alpha, Beta)) {
+	      psError(PS_ERR_UNKNOWN, false, "Unable to solve for center.");
+	      psFree (Alpha);
+	      psFree (Beta);
+	      psFree (XdX);
+	      psFree (XdY);
+	      psFree (YdX);
+	      psFree (YdY);
+		return NULL;
+	    }
+
+            Xo -= Beta->data.F32[0];
+            Yo -= Beta->data.F32[1];
+            dPos = hypot(Beta->data.F32[0], Beta->data.F32[1]);
+
+        }
+        psFree (Alpha);
+        psFree (Beta);
+        psFree (XdX);
+        psFree (XdY);
+        psFree (YdX);
+        psFree (YdY);
+
+	if (dPos > tol) {
+	    psError(PS_ERR_UNKNOWN, false, "Newton-Raphson method did not converge after 20 iterations (%f)\n", dPos);
+	    return NULL;
+	}
+    }
+    psPlane *center = psPlaneAlloc ();
+    center->x = Xo;
+    center->y = Yo;
+
+    return center;
+}
+
+// convert a transformation L(x,y) to L'(x-xo,y-yo)
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo)
+{
+
+    // validate fit order
+
+    if (output == NULL) {
+        output = psPlaneTransformAlloc(input->x->nX, input->x->nY);
+    }
+
+    /* given two equivalent polynomial representations L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j
+     * we can transform L(x,y) into L'(x-xo,y-yo) by taking the derivatives of both sides and 
+     * noting that the constant term in each is the coefficient in the case of L(x,y) and is the 
+     * value of L'(-xo,-yo) in the second case.
+     */
+
+    psPolynomial2D *tmp;
+
+    psPolynomial2D *xPx = psPolynomial2DCopy (NULL, input->x);
+    psPolynomial2D *yPx = psPolynomial2DCopy (NULL, input->y);
+
+    for (int i = 0; i <= input->x->nX; i++) {
+        psPolynomial2D *xPy = psPolynomial2DCopy (NULL, xPx);
+        psPolynomial2D *yPy = psPolynomial2DCopy (NULL, yPx);
+        for (int j = 0; j <= input->x->nY; j++) {
+            output->x->coeffMask[i][j] = input->x->coeffMask[i][j];
+            output->y->coeffMask[i][j] = input->y->coeffMask[i][j];
+            output->x->coeff[i][j] = (output->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (xPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+            output->y->coeff[i][j] = (output->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0 : psPolynomial2DEval (yPy, Xo, Yo) / tgamma(i+1) / tgamma(j+1);
+
+            // take the next derivative wrt y, catch output (is NULL on last pass)
+            tmp = psPolynomial2D_dY(NULL, xPy);
+            psFree (xPy);
+            xPy = tmp;
+            tmp = psPolynomial2D_dY(NULL, yPy);
+            psFree (yPy);
+            yPy = tmp;
+        }
+        // take the next derivative wrt x, catch output (is NULL on last pass)
+        tmp = psPolynomial2D_dX(NULL, xPx);
+        psFree (xPx);
+        xPx = tmp;
+        tmp = psPolynomial2D_dX(NULL, yPx);
+        psFree (yPx);
+        yPx = tmp;
+    }
+    return output;
+}
+
+// rotate a transformation L(x,y) by theta
+psPlaneTransform *psPlaneTransformRotate (psPlaneTransform *output, psPlaneTransform *input, double theta)
+{
+    /* given the polynomial transformations:
+     *  L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j and 
+     *  M(x,y) = \sum_i \sum_j B_{i,j} x^i y^j 
+     * we can rotate L,M to L',M' by applying the rotation matrix (c,s),(-s,c).
+     * the resulting terms of L and M are:
+     * A'_{i,j} = c A_{i,j} + s B_{i,j}
+     * B'_{i,j} = c B_{i,j} - s A_{i,j}
+     */
+
+    if (output == NULL) {
+        output = psPlaneTransformAlloc(input->x->nX, input->x->nY);
+    }
+
+    float cs = cos(theta);
+    float sn = sin(theta);
+
+    for (int i = 0; i <= input->x->nX; i++) {
+        for (int j = 0; j <= input->x->nY; j++) {
+	    // XXX what about inconsistent x and y masking?
+            output->x->coeffMask[i][j] = input->x->coeffMask[i][j];
+	    output->y->coeffMask[i][j] = input->y->coeffMask[i][j];
+	    if (output->x->coeffMask[i][j]) {
+		output->x->coeff[i][j] = 0.0;
+		output->y->coeff[i][j] = 0.0;
+	    } else {
+		output->x->coeff[i][j] = cs*input->x->coeff[i][j] + sn*input->y->coeff[i][j];
+		output->y->coeff[i][j] = cs*input->y->coeff[i][j] - sn*input->x->coeff[i][j];
+	    }
+        }
+    }
+    return output;
+}
+
+// construct a psPlaneTransform which is the identify transformation
+psPlaneTransform *psPlaneTransformIdentity (int order)
+{
+
+    psPlaneTransform *transform;
+
+    if (order < 1)
+        psAbort("invalid order");
+    if (order > 3)
+        psAbort("invalid order");
+
+    // all coeffs and masks initially set to 0
+    transform = psPlaneTransformAlloc (order, order);
+
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                transform->x->coeffMask [i][j] = PS_POLY_MASK_SET;
+                transform->y->coeffMask [i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+    transform->x->coeff[1][0] = 1;
+    transform->y->coeff[0][1] = 1;
+    transform->x->coeffMask[1][0] = PS_POLY_MASK_NONE;
+    transform->y->coeffMask[0][1] = PS_POLY_MASK_NONE;
+
+    return transform;
+}
+
+// check that the given psPlaneTransform is the identity * (Xs,Ys)
+bool psPlaneTransformIsDiagonal (psPlaneTransform *transform)
+{
+
+    int order;
+    bool status;
+
+    // we currently only support up to 3rd order polynomials
+    if (transform->x->nX < 1)
+        return false;
+    if (transform->x->nY < 1)
+        return false;
+    if (transform->y->nX < 1)
+        return false;
+    if (transform->y->nY < 1)
+        return false;
+
+    if (transform->x->nX != transform->x->nY)
+        return false;
+    if (transform->y->nX != transform->y->nY)
+        return false;
+
+    // these are not actually valid tests
+    if (transform->x->nX > 3)
+        return false;
+    if (transform->x->nY > 3)
+        return false;
+    if (transform->y->nX > 3)
+        return false;
+    if (transform->y->nY > 3)
+        return false;
+
+    status = true;
+    order = transform->x->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (transform->x->coeffMask[i][j] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(transform->x->coeffMask[i][j] & PS_POLY_MASK_SET);
+                if ((i == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be non-zero
+                    status &= (fabs(transform->x->coeff[i][j]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(transform->x->coeff[i][j]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+
+    order = transform->y->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (transform->y->coeffMask[i][j] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(transform->y->coeffMask[i][j] & PS_POLY_MASK_SET);
+                if ((j == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(transform->y->coeff[i][j]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(transform->y->coeff[i][j]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+    return status;
+}
+
+// construct a psPlaneDistort which is the identify transformation
+psPlaneDistort *psPlaneDistortIdentity (int order)
+{
+
+    psPlaneDistort *distort;
+
+    if (order < 1)
+        psAbort("invalid order");
+    if (order > 3)
+        psAbort("invalid order");
+
+    // all coeffs and masks initially set to 0
+    distort = psPlaneDistortAlloc (order, order, 0, 0);
+
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                distort->x->coeffMask [i][j][0][0] = PS_POLY_MASK_SET;
+                distort->y->coeffMask [i][j][0][0] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+    distort->x->coeff[1][0][0][0] = 1;
+    distort->y->coeff[0][1][0][0] = 1;
+
+    return distort;
+}
+
+// check that the given psPlaneDistort is the identity * (Xs,Ys)
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort)
+{
+
+    int order;
+    bool status;
+
+    // we currently only support up to 3rd order polynomials
+    if (distort->x->nX < 1)
+        return false;
+    if (distort->x->nY < 1)
+        return false;
+    if (distort->y->nX < 1)
+        return false;
+    if (distort->y->nY < 1)
+        return false;
+
+    if (distort->x->nX > 3)
+        return false;
+    if (distort->x->nY > 3)
+        return false;
+    if (distort->y->nX > 3)
+        return false;
+    if (distort->y->nY > 3)
+        return false;
+
+    if (distort->x->nZ > 0)
+        return false;
+    if (distort->x->nT > 0)
+        return false;
+    if (distort->y->nZ > 0)
+        return false;
+    if (distort->y->nT > 0)
+        return false;
+
+    if (distort->x->nX != distort->x->nY)
+        return false;
+    if (distort->y->nX != distort->y->nY)
+        return false;
+
+    status = true;
+    order = distort->x->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (distort->x->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(distort->x->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+                if ((i == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(distort->x->coeff[i][j][0][0]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(distort->x->coeff[i][j][0][0]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+
+    order = distort->y->nX;
+    for (int i = 0; i <= order; i++) {
+        for (int j = 0; j <= order; j++) {
+            if (i + j > order) {
+                // high-order cross terms must be masked (eg, x^3 y^2)
+                status &= (distort->y->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+            } else {
+                status &= !(distort->y->coeffMask[i][j][0][0] & PS_POLY_MASK_SET);
+                if ((j == 1) && (i + j == 1)) {
+                    // linear, diagonal terms must be 1.0
+                    status &= (fabs(distort->y->coeff[i][j][0][0]) > FLT_EPSILON);
+                } else {
+                    // non-linear and off-diagonal terms must be 0 (eg, x^2, x y)
+                    status &= (fabs(distort->y->coeff[i][j][0][0]) < FLT_EPSILON);
+                }
+            }
+        }
+    }
+    return status;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryUtils.h	(revision 22306)
@@ -0,0 +1,28 @@
+/* @file  pmAstrometryUtils.h
+ * @brief utility functions for transform and distort functions
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.4 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-12-19 18:57:05 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_UTILS_H
+#define PM_ASTROMETRY_UTILS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+psPlane *psPlaneTransformGetCenter (psPlaneTransform *trans, double tol);
+psPlaneTransform *psPlaneTransformSetCenter (psPlaneTransform *output, psPlaneTransform *input, double Xo, double Yo);
+psPlaneTransform *psPlaneTransformRotate (psPlaneTransform *output, psPlaneTransform *input, double theta);
+
+psPlaneTransform *psPlaneTransformIdentity (int order);
+psPlaneDistort *psPlaneDistortIdentity (int order);
+
+bool psPlaneTransformIsDiagonal (psPlaneTransform *transform);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+/// @}
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.c	(revision 22306)
@@ -0,0 +1,829 @@
+/** @file  pmAstrometryWCS.c
+ *
+ *  @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ *  @ingroup Astrometry
+ *
+ *  @author EAM, IfA
+ *
+ *  @version $Revision: 1.29 $ $Name: not supported by cvs2svn $
+ *  @date $Date: 2008-09-12 01:05:59 $
+ *
+ *  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);
+    // 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);
+    }
+
+    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)
+{
+    // 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 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: /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psModules/src/astrom/pmAstrometryWCS.h	(revision 22306)
@@ -0,0 +1,77 @@
+/* @file  pmAstrometryDistortion.h
+ * @brief functions to convert FITS WCS keywords to / from pmFPA structures
+ *
+ * @author EAM, IfA
+ *
+ * @version $Revision: 1.11 $ $Name: not supported by cvs2svn $
+ * @date $Date: 2007-03-21 22:00:49 $
+ * Copyright 2006 Institute for Astronomy, University of Hawaii
+ */
+
+#ifndef PM_ASTROMETRY_WCS_H
+#define PM_ASTROMETRY_WCS_H
+
+/// @addtogroup Astrometry
+/// @{
+
+#define PM_ASTROM_WCS_TYPE_SIZE 80
+typedef struct
+{
+    char ctype1[PM_ASTROM_WCS_TYPE_SIZE];
+    char ctype2[PM_ASTROM_WCS_TYPE_SIZE];
+    double crval1, crval2;
+    double crpix1, crpix2;
+    double cdelt1, cdelt2;
+    psProjection *toSky;
+    psPlaneTransform *trans;
+}
+pmAstromWCS;
+
+// support function for the pmAstromWCS representation
+pmAstromWCS *pmAstromWCSAlloc (int nXorder, int nYorder);
+bool pmAstromWCStoSky (psSphere *sky, pmAstromWCS *wcs, psPlane *chip);
+bool pmAstromWCStoChip (psPlane *chip, pmAstromWCS *wcs, psSphere *sky);
+
+// read and write the pmAstromWCS representation to the header
+bool pmAstromWCStoHeader (psMetadata *header, const pmAstromWCS *wcs);
+pmAstromWCS *pmAstromWCSfromHeader (const psMetadata *header);
+
+// convert from wcs terms to chip->toFPA, fpa->toSky,toTPA terms
+bool pmAstromWCSBileveltoChip (pmChip *chip, const pmAstromWCS *wcs);
+bool pmAstromWCSBileveltoFPA (pmFPA *fpa, const pmAstromWCS *wcs, psRegion region);
+
+// convert from chip->toFPA, fpa->toSky,toTPA terms to wcs terms
+pmAstromWCS *pmAstromWCSBilevelChipFromFPA (const pmChip *chip, double tol);
+pmAstromWCS *pmAstromWCSBilevelMosaicFromFPA (const pmFPA *fpa, double tol);
+
+// convert the pmAstromWCS representation to the FPA representation
+bool pmAstromWCStoFPA (pmFPA *fpa, pmChip *chip, const pmAstromWCS *wcs, double plateScale);
+pmAstromWCS *pmAstromWCSfromFPA (const pmFPA *fpa, const pmChip *chip, double tol);
+
+// read wcs terms from the supplied header into the fpa hierarchy components
+bool pmAstromReadWCS (pmFPA *fpa, pmChip *chip, const psMetadata *header, double plateScale);
+
+// write the wcs terms from the fpa hierarchy components into the supplied header
+// tol is the convergence tolerance for the non-linear solution to the reference pixel
+bool pmAstromWriteWCS (psMetadata *header, const pmFPA *fpa, const pmChip *chip, double tol);
+
+bool pmAstromReadBilevelChip (pmChip *chip, const psMetadata *header);
+bool pmAstromReadBilevelMosaic (pmFPA *fpa, const psMetadata *header);
+
+bool pmAstromWriteBilevelChip (psMetadata *header, const pmChip *chip, double tol);
+bool pmAstromWriteBilevelMosaic (psMetadata *header, const pmFPA *fpa, double tol);
+
+// move to pslib
+psPlaneDistort *psPlaneDistortIdentity (int order);
+bool psPlaneDistortIsDiagonal (psPlaneDistort *distort);
+
+# define PM_DEG_RAD 57.295779513082322
+# define PM_RAD_DEG  0.017453292519943
+
+/// @}
+#endif // PM_ASTROMETRY_WCS_H
+
+/*
+ * the wcs->trans component defines a polynomial which converts (x-crpix1),(y-crpix2) to
+ * L,M in degrees
+ */
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/.cvsignore
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/.cvsignore	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/.cvsignore	(revision 22306)
@@ -0,0 +1,18 @@
+bin
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.log
+config.status
+configure
+depcomp
+install-sh
+missing
+config.guess
+libtool
+ltmain.sh
+stamp-h1
+config.sub
+psastro.pc
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/Makefile.am
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/Makefile.am	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/Makefile.am	(revision 22306)
@@ -0,0 +1,10 @@
+SUBDIRS = src
+
+CLEANFILES = *.pyc *~ core core.*
+
+pkgconfigdir = $(libdir)/pkgconfig
+pkgconfig_DATA= psastro.pc
+
+EXTRA_DIST = \
+	psastro.pc.in \
+	autogen.sh
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/autogen.sh
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/autogen.sh	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/autogen.sh	(revision 22306)
@@ -0,0 +1,103 @@
+#!/bin/sh
+# Run this to generate all the initial makefiles, etc.
+
+srcdir=`dirname $0`
+test -z "$srcdir" && srcdir=.
+
+ORIGDIR=`pwd`
+cd $srcdir
+
+PROJECT=psastro
+TEST_TYPE=-f
+# change this to be a unique filename in the top level dir
+FILE=autogen.sh
+
+DIE=0
+
+LIBTOOLIZE=libtoolize
+ACLOCAL="aclocal $ACLOCAL_FLAGS"
+AUTOHEADER=autoheader
+AUTOMAKE=automake
+AUTOCONF=autoconf
+
+($LIBTOOLIZE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $LIBTOOLIZE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/libtool/"
+        DIE=1
+}
+
+($ACLOCAL --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $ACLOCAL installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOHEADER --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOHEADER installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+($AUTOMAKE --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOMAKE installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/automake/"
+        DIE=1
+}
+
+($AUTOCONF --version) < /dev/null > /dev/null 2>&1 || {
+        echo
+        echo "You must have $AUTOCONF installed to compile $PROJECT."
+        echo "Download the appropriate package for your distribution,"
+        echo "or get the source tarball at http://ftp.gnu.org/gnu/autoconf/"
+        DIE=1
+}
+
+if test "$DIE" -eq 1; then
+        exit 1
+fi
+
+test $TEST_TYPE $FILE || {
+        echo "You must run this script in the top-level $PROJECT directory"
+        exit 1
+}
+
+if test -z "$*"; then
+        echo "I am going to run ./configure with no arguments - if you wish "
+        echo "to pass any to it, please specify them on the $0 command line."
+fi
+
+$LIBTOOLIZE --copy --force || echo "$LIBTOOLIZE failed"
+$ACLOCAL || echo "$ACLOCAL failed"
+$AUTOHEADER || echo "$AUTOHEADER failed"
+$AUTOMAKE --add-missing --force-missing --copy || echo "$AUTOMAKE  failed"
+$AUTOCONF || echo "$AUTOCONF failed"
+
+cd $ORIGDIR
+
+run_configure=true
+for arg in $*; do
+    case $arg in
+        --no-configure)
+            run_configure=false
+            ;;
+        *)
+            ;;
+    esac
+done
+
+if $run_configure; then
+    $srcdir/configure --enable-maintainer-mode "$@"
+    echo
+    echo "Now type 'make' to compile $PROJECT."
+else
+    echo
+    echo "Now run 'configure' and 'make' to compile $PROJECT."
+fi
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/configure.ac
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/configure.ac	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/configure.ac	(revision 22306)
@@ -0,0 +1,204 @@
+dnl Process this file with autoconf to produce a configure script.
+AC_PREREQ(2.61)
+
+AC_INIT([psastro], [0.9.0], [ipp-support@ifa.hawaii.edu])
+AC_CONFIG_SRCDIR([src])
+
+AM_INIT_AUTOMAKE([1.6 foreign dist-bzip2])
+AM_CONFIG_HEADER([src/config.h])
+AM_MAINTAINER_MODE
+
+IPP_STDCFLAGS
+
+AC_LANG(C)
+AC_GNU_SOURCE
+AC_PROG_CC_C99
+AC_PROG_INSTALL
+AC_PROG_LIBTOOL
+AC_SYS_LARGEFILE
+
+dnl ------------------------------------------------------------
+
+AC_PATH_PROG([ERRORCODES], [psParseErrorCodes], [missing])
+if test "$ERRORCODES" = "missing" ; then
+  AC_MSG_ERROR([psParseErrorCodes is required])
+fi
+
+dnl ------------------ kapa,libkapa options -------------------------
+dnl -- libkapa implies the requirement for libpng, libjpeg as well --
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+dnl test for command-line options: use ohana-config if not supplied
+KAPA_CFLAGS_CONFIG="true"
+KAPA_LIBS_CONFIG="true"
+AC_ARG_WITH(kapa,
+[AS_HELP_STRING(--with-kapa=DIR,Specify location of libkapa)],
+[KAPA_CFLAGS="-I$withval/include" KAPA_LIBS="-L$withval/lib" 
+ KAPA_CFLAGS_CONFIG="false"       KAPA_LIBS_CONFIG="false"])
+AC_ARG_WITH(kapa-include,
+[AS_HELP_STRING(--with-kapa-include=DIR,Specify libkapa include directory.)],
+[KAPA_CFLAGS="-I$withval" KAPA_CFLAGS_CONFIG="false"])
+AC_ARG_WITH(kapa-lib,
+[AS_HELP_STRING(--with-kapa-lib=DIR,Specify libkapa library directory.)],
+[KAPA_LIBS="-L$withval" KAPA_LIBS_CONFIG="false"])
+
+echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+echo "KAPA_LIBS_CONFIG: $KAPA_LIBS_CONFIG"
+echo "KAPA_CFLAGS: $KAPA_CFLAGS"
+echo "KAPA_LIBS: $KAPA_LIBS"
+
+dnl HAVE_KAPA is set to false if any of the tests fail
+HAVE_KAPA="true"
+AC_MSG_NOTICE([checking for libkapa])
+if test "$KAPA_CFLAGS_CONFIG" = "true" -o "$KAPA_LIBS_CONFIG" = "true"; then
+  AC_MSG_NOTICE([kapa info supplied by ohana-config])
+  KAPA_CONFIG=`which ohana-config`
+  AC_CHECK_FILE($KAPA_CONFIG,[],
+    [HAVE_KAPA="false"; AC_MSG_WARN([libkapa is not found: output plots disabled.  Obtain libkapa at http://kiawe.ifa.hawaii.edu/Elixir/Ohana or use --with-kapa to specify location])])
+  
+  echo "HAVE_KAPA: $HAVE_KAPA"
+  echo "KAPA_CFLAGS_CONFIG: $KAPA_CFLAGS_CONFIG"
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_CFLAGS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa cflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa cflags])
+   KAPA_CFLAGS="`${KAPA_CONFIG} --cflags`"
+   AC_MSG_RESULT([${KAPA_CFLAGS}])
+  fi
+
+  if test "$HAVE_KAPA" = "true" -a "$KAPA_LIBS_CONFIG" = "true" ; then
+   AC_MSG_NOTICE([libkapa ldflags info supplied by ohana-config])
+   AC_MSG_CHECKING([libkapa ldflags])
+   KAPA_LIBS="`${KAPA_CONFIG} --libs` -lX11"
+   AC_MSG_RESULT([${KAPA_LIBS}])
+  fi
+fi
+
+if test "$HAVE_KAPA" = "true" ; then
+ AC_MSG_NOTICE([libkapa supplied])
+ PSASTRO_CFLAGS="${PSASTRO_CFLAGS} ${KAPA_CFLAGS}"
+ PSASTRO_LIBS="${PSASTRO_LIBS} ${KAPA_LIBS}"
+else
+ AC_MSG_NOTICE([libkapa ignored])
+fi
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libjpeg options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(jpeg,
+[AS_HELP_STRING(--with-jpeg=DIR,Specify location of libjpeg.)],
+[JPEG_CFLAGS="-I$withval/include"
+ JPEG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(jpeg-include,
+[AS_HELP_STRING(--with-jpeg-include=DIR,Specify libjpeg include directory.)],
+[JPEG_CFLAGS="-I$withval"])
+AC_ARG_WITH(jpeg-lib,
+[AS_HELP_STRING(--with-jpeg-lib=DIR,Specify libjpeg library directory.)],
+[JPEG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${JPEG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${JPEG_LDFLAGS}"
+
+AC_CHECK_HEADERS([jpeglib.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $JPEG_CFLAGS" AC_SUBST(JPEG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg headers not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+AC_CHECK_LIB(jpeg,jpeg_CreateCompress,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $JPEG_LDFLAGS -ljpeg"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libjpeg library not found: output plots disabled.  Obtain libjpeg from http://www.ijg.org/ or use --with-jpeg to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ libpng options ---------------------
+
+dnl save LIBS/CFLAGS/LDFLAGS
+TMP_LIBS=${LIBS}
+TMP_CFLAGS=${CFLAGS}
+TMP_LDFLAGS=${LDFLAGS}
+TMP_CPPFLAGS=${CPPFLAGS}
+
+AC_ARG_WITH(png,
+[AS_HELP_STRING(--with-png=DIR,Specify location of libpng.)],
+[PNG_CFLAGS="-I$withval/include"
+ PNG_LDFLAGS="-L$withval/lib"])
+AC_ARG_WITH(png-include,
+[AS_HELP_STRING(--with-png-include=DIR,Specify libpng include directory.)],
+[PNG_CFLAGS="-I$withval"])
+AC_ARG_WITH(png-lib,
+[AS_HELP_STRING(--with-png-lib=DIR,Specify libpng library directory.)],
+[PNG_LDFLAGS="-L$withval"])
+
+CFLAGS="${CFLAGS} ${PNG_CFLAGS}"
+CPPFLAGS=${CFLAGS}
+LDFLAGS="${LDFLAGS} ${PNG_LDFLAGS}"
+
+AC_CHECK_HEADERS([png.h],
+  [PSASTRO_CFLAGS="$PSASTRO_CFLAGS $PNG_CFLAGS" AC_SUBST(PNG_CFLAGS)],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng headers not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+AC_CHECK_LIB(png,png_init_io,
+  [PSASTRO_LIBS="$PSASTRO_LIBS $PNG_LDFLAGS -lpng"],
+  [HAVE_KAPA=false; AC_MSG_WARN([libpng library not found: output plots disabled.  Obtain libpng from http://www.ijg.org/ or use --with-png to specify location.])]
+)
+
+dnl restore the CFLAGS/LDFLAGS
+LIBS=${TMP_LIBS}
+CFLAGS=${TMP_CFLAGS}
+LDFLAGS=${TMP_LDFLAGS}
+CPPFLAGS=${TMP_CPPFLAGS}
+
+dnl ------------------ use kapa or not? ---------------------
+
+if test "$HAVE_KAPA" == "true" ; then
+  AC_MSG_RESULT([including plotting functions])
+  AC_DEFINE([HAVE_KAPA],[1],[enable use of libkapa])
+else
+  AC_MSG_RESULT([skipping plotting functions])
+  AC_DEFINE([HAVE_KAPA],[0],[disable use of libkapa])
+fi
+
+dnl ------------- psLib, psModules ---------------
+PKG_CHECK_MODULES([PSLIB],    [pslib >= 1.0.0])
+PKG_CHECK_MODULES([PSMODULE], [psmodules >= 1.0.0])
+PKG_CHECK_MODULES([PPSTATS],  [ppStats >= 1.0.0]) 
+
+dnl Set CFLAGS for build
+IPP_STDOPTS
+CFLAGS="${CFLAGS=} -Wall -Werror"
+echo "PSASTRO_CFLAGS: $PSASTRO_CFLAGS"
+echo "PSASTRO_LIBS: $PSASTRO_LIBS"
+
+AC_SUBST([PSASTRO_CFLAGS])
+AC_SUBST([PSASTRO_LIBS])
+
+AC_CONFIG_FILES([
+  Makefile
+  src/Makefile
+  psastro.pc
+])
+
+AC_OUTPUT
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/mktest.txt
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/mktest.txt	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/mktest.txt	(revision 22306)
@@ -0,0 +1,31 @@
+
+the program, psastro-mktest, will generate a set of fake test data for
+psastro.  the config file 'psastro-mktest.conf' gives an example setup
+for this program.  It generates 4 output files:
+
+- cmpfile : this is a basic elixir-style cmp file (stars plus header)
+  which can be taken as input to psastro
+
+- catfile : this is a basic text reference catalog, which the current
+  psastro loads as a reference
+
+- rawfile : this is a text listing of all astrometry stages for the
+  generated stars: readout, cell, chip, fpa, tpa, sky, with the last
+  two columns being the magnitudes.  this file is generated by
+  applying the transformations starting at the readout and going
+  step-by-step to the sky.
+
+- reffile : this is the inverse of the preceeding file, with the
+  resulting sources transformed from the sky coordinates down
+  step-by-step to the readout pixels.  If the transformations are
+  functioning, these two files (rawfile & reffile) should be identical
+  within floating point errors.
+
+examples:
+
+build a fake dataset
+# psastro-mktest doc/psastro-mktest.conf test.cmp test.cat test.raw test.ref
+
+run psastrom on the fake data:
+# psastro doc/psastro.conf test.cmp test.dat
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/notes.txt
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/notes.txt	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/notes.txt	(revision 22306)
@@ -0,0 +1,39 @@
+
+2006.12.26
+
+ work still to be done for psastro:
+
+ - test suite
+   - create a DVO database from a fake population
+   - create fake images with range of errors drawn from DVO
+
+ - 
+
+
+we have a few different astrometry circumstances for our complete
+image/chip hierarchy structure:
+
+- ChipAstrom with one readout, one cell per chip:
+
+  whether or not there is more than one chip, we need to have two
+  stages
+  
+    - in the first stage, the per-chip results are applied to the
+      chip.toFPA parameter set.  
+
+    - in the second stage, the chip results are collectively used to
+      modify the fpa.toSky terms.
+
+      - the average offsets of the chip positions relative to starting
+	coordinates are used to calculate a single average offset to
+	the boresite. 
+
+      - the average rotations of the chips relative to their starting
+        rotations are used to calculate a single average rotation of
+        the boresite
+
+
+- ChipAstrom with more than one readout and/or cell:
+
+  - match all stars up relative to first readout?
+   
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro-mktest.conf
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro-mktest.conf	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro-mktest.conf	(revision 22306)
@@ -0,0 +1,28 @@
+
+NX            S32  2000    	 # chip dimensions
+NY            S32  2000    	 # chip dimensions
+
+NSTARS        S32  20    	 # number of fake stars to generate
+
+# the current test output file only has the basic WCS info.  
+READOUT.COL.0    F32  0		 # cell coord for readout (0,0)
+READOUT.ROW.0    F32  0		 # cell coord for readout (0,0)
+READOUT.COL.BIN  F32  1		 # readout col binning factor
+READOUT.ROW.BIN  F32  1		 # readout row binning factor
+
+CELL.COL.0    	 F32  0		 # chip coord for cell (0,0)
+CELL.ROW.0    	 F32  0		 # chip coord for cell (0,0)
+
+CHIP.COL.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.ROW.0    	 F32  0		 # FPA coord for chip (0,0)
+CHIP.THETA    	 F32  0	         # cw rotation from chip to FPA
+CHIP.PARITY.X 	 F32  1		 # x flip 
+CHIP.PARITY.Y 	 F32  1		 # y flip 
+
+FPA.THETA     	 F32  0		 # fpa row offset
+FPA.COL.0  	 F32  0		 # TPA coord for FPA (0,0)
+FPA.ROW.0  	 F32  0		 # TPA coord for FPA (0,0)
+
+PLATE.SCALE	 F32  1	         # plate scale in arcsec/pixel
+RA		 F32  10	 # celestial coord for TPA (0,0)
+DEC     	 F32  20	 # celestial coord for TPA (0,0)
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro.conf
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro.conf	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/doc/psastro.conf	(revision 22306)
@@ -0,0 +1,17 @@
+
+PSASTRO.MATCH.RADIUS 	F32 2.0    # fitting radius in pixels
+
+PSASTRO.CHIP.NX      	S32 1      # chip-to-fpa tranformation order
+PSASTRO.CHIP.NY	     	S32 1      # chip-to-fpa tranformation order
+
+PSASTRO.CHIP.NSIGMA  	F32 3.0    # Nsigma clip for matched-fit 
+PSASTRO.CHIP.NITER   	S32 3      # number of clipping interations for matched-fit 
+
+PSASTRO.GRID.OFFSET  	F32 100.0  # maximum allowed offset for grid match
+PSASTRO.GRID.SCALE   	F32  50.0  # binning scale for grid match
+
+PSASTRO.GRID.MIN.ANGLE  F32   0.0  # starting angle
+PSASTRO.GRID.MAX.ANGLE  F32   0.0  # ending angle
+PSASTRO.GRID.DEL.ANGLE  F32   0.5  # steps between angle attempts
+
+PSASTRO.STARS.MAX       S32 300    # use no more than this number of sources
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/psastro.pc.in
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/psastro.pc.in	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/psastro.pc.in	(revision 22306)
@@ -0,0 +1,12 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: libpsastro
+Description: Pan-STARRS Photometry Library
+Version: @VERSION@
+Requires: pslib psmodules
+Libs: -L${libdir} -lpsastro
+Libs.private: @PSASTRO_LIBS@
+Cflags: -I${includedir} @PSASTRO_CFLAGS@
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/.cvsignore
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/.cvsignore	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/.cvsignore	(revision 22306)
@@ -0,0 +1,17 @@
+*.o
+*.lo
+.libs
+.deps
+Makefile
+Makefile.in
+psastro
+psastro.loT
+psastroErrorCodes.h
+psastroErrorCodes.c
+libpsastro.la
+config.h
+config.h.in
+stamp-h1
+psastroModel
+gpcModel
+psastroModelFit
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/Makefile.am
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/Makefile.am	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/Makefile.am	(revision 22306)
@@ -0,0 +1,109 @@
+
+lib_LTLIBRARIES = libpsastro.la
+libpsastro_la_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+libpsastro_la_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+
+bin_PROGRAMS = psastro psastroModel psastroModelFit gpcModel
+
+psastro_CFLAGS = $(PSASTRO_CFLAGS) $(PPSTATS_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastro_LDFLAGS = $(PSASTRO_LIBS) $(PPSTATS_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastro_LDADD = libpsastro.la
+
+psastroModel_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastroModel_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastroModel_LDADD = libpsastro.la
+
+psastroModelFit_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psastroModelFit_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psastroModelFit_LDADD = libpsastro.la
+
+gpcModel_CFLAGS = $(PSASTRO_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+gpcModel_LDFLAGS = $(PSASTRO_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+gpcModel_LDADD = libpsastro.la
+
+psastro_SOURCES = \
+	psastro.c		    \
+        psastroArguments.c	    \
+	psastroParseCamera.c   	    \
+	psastroDataLoad.c           \
+	psastroDataSave.c           \
+	psastroMetadataStats.c      \
+	psastroCleanup.c
+
+psastroModel_SOURCES = \
+	psastroModel.c		    \
+        psastroModelArguments.c	    \
+	psastroModelParseCamera.c   \
+	psastroModelDataLoad.c      \
+	psastroModelAnalysis.c      \
+	psastroModelBoresite.c      \
+	psastroModelFitBoresite.c   \
+	psastroModelAdjust.c        \
+	psastroModelDataSave.c      \
+	psastroCleanup.c
+
+psastroModelFit_SOURCES = \
+	psastroModelFit.c \
+	psastroModelBoresite.c      \
+	psastroModelFitBoresite.c
+
+gpcModel_SOURCES = gpcModel.c
+
+libpsastro_la_SOURCES = \
+	psastroErrorCodes.c         \
+	psastroVersion.c            \
+	psastroDefineFiles.c        \
+	psastroAnalysis.c           \
+	psastroAstromGuess.c        \
+	psastroLoadRefstars.c       \
+	psastroChooseRefstars.c     \
+	psastroConvert.c	    \
+	psastroChipAstrom.c         \
+	psastroOneChipGrid.c	    \
+	psastroOneChipFit.c	    \
+	psastroRemoveClumps.c	    \
+	psastroUtils.c	       	    \
+	psastroTestFuncs.c          \
+	psastroLuminosityFunction.c \
+	psastroRefstarSubset.c      \
+	psastroFixChips.c           \
+	psastroFixChipsTest.c       \
+	psastroUseModel.c           \
+	psastroMosaicAstrom.c       \
+	psastroMosaicDistortion.c   \
+	psastroMosaicFPtoTP.c       \
+	psastroMosaicGradients.c    \
+	psastroMosaicCorrectDistortion.c   \
+	psastroMosaicChipAstrom.c   \
+	psastroMosaicOneChip.c      \
+	psastroMosaicSetAstrom.c    \
+	psastroMosaicSetMatch.c     \
+	psastroZeroPoint.c    	    \
+	psastroDemoDump.c           \
+	psastroDemoPlot.c
+
+include_HEADERS = \
+	psastro.h \
+	psastroErrorCodes.h
+
+noinst_HEADERS = \
+	psastroInternal.h \
+	psastroStandAlone.h
+
+clean-local:
+	-rm -f TAGS
+
+# Tags for emacs
+tags:
+	etags `find . -name \*.[ch] -print`
+
+### Error codes.
+BUILT_SOURCES = psastroErrorCodes.h psastroErrorCodes.c
+CLEANFILES = psastroErrorCodes.h psastroErrorCodes.c
+EXTRA_DIST = psastroErrorCodes.dat psastroErrorCodes.h.in psastroErrorCodes.c.in
+
+psastroErrorCodes.h : psastroErrorCodes.dat psastroErrorCodes.h.in
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.h
+
+psastroErrorCodes.c : psastroErrorCodes.dat psastroErrorCodes.c.in psastroErrorCodes.h
+	$(ERRORCODES) --data=psastroErrorCodes.dat --outdir=. psastroErrorCodes.c
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/gpcModel.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/gpcModel.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/gpcModel.c	(revision 22306)
@@ -0,0 +1,165 @@
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    // USAGE: gpcModel (input) (output)
+    // generate model for gpc based on input model for all but chips
+
+    if (argc != 3) {
+	fprintf (stderr, "USAGE: gpcModel (input) (output)\n");
+	exit (2);
+    }
+
+    psFits *input = psFitsOpen (argv[1], "r");
+    psFits *output = psFitsOpen (argv[2], "w");
+
+    { // PHU
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psFitsWriteBlank (output, header, "");
+    }
+
+    { // CHIPS
+
+	// read initial chips
+	if (!psFitsMoveExtName (input, "CHIPS")) {
+	    psError(PS_ERR_IO, false, "missing CHIPS extension in astrometry table\n");
+	    return false;
+	}
+	psArray *chips = psFitsReadTable (input);
+	if (!chips) psAbort("cannot read chips");
+	fprintf (stderr, "read %ld rows from CHIPS\n", chips->n);
+
+	psMetadata *header = psMetadataAlloc();
+	psMetadataAddStr(header, PS_LIST_TAIL, "COORD",    PS_META_REPLACE, "name of this layer",	"CHIPS");
+	psMetadataAddStr(header, PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",     	"FOCAL_PLANE");
+	psMetadataAddStr(header, PS_LIST_TAIL, "BOUNDARY", PS_META_REPLACE, "validity region",   	"RECTANGLE");
+	psMetadataAddStr(header, PS_LIST_TAIL, "TRANSFRM", PS_META_REPLACE, "mapping to parent", 	"POLYNOMIAL");
+
+	float coeff[2][2];
+	char coeffMask[2][2];
+	coeff[0][0] = 0.0;
+	coeff[1][0] = 1.0;
+	coeff[0][1] = 0.0;
+	coeff[1][1] = 0.0;
+
+	coeffMask[0][0] = 0;
+	coeffMask[1][0] = 0;
+	coeffMask[0][1] = 0;
+	coeffMask[1][1] = 1;
+
+	psArray *table = psArrayAllocEmpty (1);
+	for (int i = 0; i < 8; i++) {
+	    for (int j = 0; j < 8; j++) {
+		if ((i == 0) && (j == 0)) continue;
+		if ((i == 0) && (j == 7)) continue;
+		if ((i == 7) && (j == 0)) continue;
+		if ((i == 7) && (j == 7)) continue;
+
+		float Xo;
+		float Yo;
+		if (i < 4) {
+		    Xo = (3 - i)*4970.0 +  60.0;
+		    Yo = (3 - j)*5133.0 + 125.0;
+		    coeff[1][0] = +1.0;
+		} else {
+		    Xo = (4 - i)*4970.0 -  60.0;
+		    Yo = (4 - j)*5133.0 - 150.0;
+		    coeff[1][0] = -1.0;
+		}	    
+
+		for (int ix = 0; ix < 2; ix++) {
+		    for (int iy = 0; iy < 2; iy++) {
+
+			psMetadata *row = psMetadataAlloc ();
+		
+			char chipname[80];
+			sprintf (chipname, "XY%d%d", i, j);
+			psMetadataAddStr(row,    PS_LIST_TAIL, "SEGMENT",  PS_META_REPLACE, "name of this segment", chipname);
+			psMetadataAddStr(row,    PS_LIST_TAIL, "PARENT",   PS_META_REPLACE, "next layer up",        "FOCAL_PLANE");
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MINX",     PS_META_REPLACE, "range", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MAXX",     PS_META_REPLACE, "range", 4846.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MINY",     PS_META_REPLACE, "range", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "MAXY",     PS_META_REPLACE, "range", 4868.0);
+
+			psMetadataAddS32(row,    PS_LIST_TAIL, "XORDER",   PS_META_REPLACE, "", ix);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "YORDER",   PS_META_REPLACE, "", iy);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "NXORDER",  PS_META_REPLACE, "", 1);
+			psMetadataAddS32(row,    PS_LIST_TAIL, "NYORDER",  PS_META_REPLACE, "", 1);
+			if ((ix == 0) && (iy == 0)) {
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", Xo);
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", Yo);
+			} else {
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_X",   PS_META_REPLACE, "", coeff[ix][iy]);
+			    psMetadataAddF32(row,    PS_LIST_TAIL, "POLY_Y",   PS_META_REPLACE, "", coeff[iy][ix]);
+			}
+			psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_X",  PS_META_REPLACE, "", 0.0);
+			psMetadataAddF32(row,    PS_LIST_TAIL, "ERROR_Y",  PS_META_REPLACE, "", 0.0);
+			psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_X",   PS_META_REPLACE, "", coeffMask[ix][iy]);
+			psMetadataAddU8 (row,    PS_LIST_TAIL, "MASK_Y",   PS_META_REPLACE, "", coeffMask[ix][iy]);
+			psArrayAdd (table, 100, row);
+			psFree (row);
+		    }
+		}
+	    }
+	}
+    
+	if (!psFitsWriteTable (output, header, table, "CHIPS")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // FP
+	if (!psFitsMoveExtName (input, "FP")) {
+	    psError(PS_ERR_IO, false, "missing FP extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *table = psFitsReadTable (input);
+	if (!table) psAbort("cannot read fp");
+	fprintf (stderr, "read %ld rows from FP\n", table->n);
+
+	if (!psFitsWriteTable (output, header, table, "FP")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // TP
+	if (!psFitsMoveExtName (input, "TP")) {
+	    psError(PS_ERR_IO, false, "missing TP extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *table = psFitsReadTable (input);
+	if (!table) psAbort("cannot read tp");
+	fprintf (stderr, "read %ld rows from TP\n", table->n);
+	if (!psFitsWriteTable (output, header, table, "TP")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(table);
+	    return false;
+	}
+    }
+
+    { // SKY
+	if (!psFitsMoveExtName (input, "SKY")) {
+	    psError(PS_ERR_IO, false, "missing SKY extension in astrometry table\n");
+	    return false;
+	}
+	psMetadata *header = psFitsReadHeader (NULL, input);
+	psArray *sky = psFitsReadTable (input);
+	if (!sky) psAbort("cannot read sky");
+	fprintf (stderr, "read %ld rows from SKY\n", sky->n);
+	if (!psFitsWriteTable (output, header, sky, "SKY")) {
+	    psError(PS_ERR_IO, false, "writing sky data\n");
+	    psFree(sky);
+	    return false;
+	}
+    }
+
+    psFitsClose (input);
+    psFitsClose (output);
+    exit (0);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.c	(revision 22306)
@@ -0,0 +1,53 @@
+# include "psastroStandAlone.h"
+
+static void usage (void) {
+    fprintf (stderr, "USAGE: psastro [-file image(s)] [-list imagelist] (output)\n");
+    exit (2);
+}
+
+int main (int argc, char **argv) {
+
+    pmConfig *config = NULL;
+
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit ();
+
+    // load configuration information
+    config = psastroArguments (argc, argv);
+    if (!config) usage ();
+
+    // load identify the data sources
+    if (!psastroParseCamera (config)) {
+	psErrorStackPrint(stderr, "error setting up the camera\n");
+	exit (1);
+    }
+
+    // load the raw detection data (using PSPHOT.SOURCES filerule)
+    // select subset of stars for astrometry
+    if (!psastroDataLoad (config)) {
+	psErrorStackPrint(stderr, "error loading input data\n");
+	exit (1);
+    }
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroAnalysis (config)) {
+	psErrorStackPrint(stderr, "failure in psastro analysis\n");
+	exit (1);
+    }
+    
+    // write out the results
+    if (!psastroDataSave (config)) {
+	psErrorStackPrint(stderr, "failed to write out data\n");
+	exit (1);
+    }
+
+    psLogMsg ("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    psastroCleanup (config);
+    exit (EXIT_SUCCESS);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastro.h	(revision 22306)
@@ -0,0 +1,107 @@
+/* This file defines the library functions available to external programs.  It must be included
+ * by programs which are compiled against psphot functions.
+ */
+
+# ifndef PSASTRO_H
+# define PSASTRO_H
+
+# include "psastroErrorCodes.h"
+# define PSASTRO_RECIPE "PSASTRO" // Name of the recipe to use
+
+# define psMemCopy(A)(psMemIncrRefCounter((A)))
+# define DEG_RAD 57.295779513082322
+# define RAD_DEG  0.017453292519943
+# define SIGN(X)  (((X) == 0) ? 0 : ((fabs((double)(X))) / (X)))
+
+// this structure represents 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;
+
+bool              psastroDataSave (pmConfig *config);
+bool              psastroDefineFiles (pmConfig *config, pmFPAfile *input);
+bool              psastroDefineFile (pmConfig *config, pmFPA *input, char *filerule, char *argname, pmFPAfileType fileType, pmDetrendType detrendType);
+bool              psastroAnalysis (pmConfig *config);
+
+bool              psastroConvertFPA (pmFPA *fpa, psMetadata *recipe);
+bool              psastroConvertReadout (pmReadout *readout, psMetadata *recipe);
+psArray          *pmSourceToAstromObj (psArray *sources);
+bool              psastroAstromGuess (int *nStars, pmConfig *config);
+bool              psastroAstromGuessCheck (pmConfig *config);
+
+psPlaneDistort   *psPlaneDistortIdentity ();
+bool              psPlaneDistortIsIdentity (psPlaneDistort *distort);
+
+psArray          *psastroLoadRefstars (pmConfig *config);
+bool              psastroChipAstrom (pmConfig *config);
+bool              psastroOneChip (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroOneChipGrid (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroOneChipFit (pmFPA *fpa, pmChip *chip, psArray *refset, psArray *rawset, psMetadata *recipe, psMetadata *updates);
+bool              psastroChooseRefstars (pmConfig *config, psArray *refs);
+bool              psastroRefstarSubset (pmReadout *readout);
+pmLumFunc        *psastroLuminosityFunction (psArray *stars);
+psArray          *psastroRemoveClumps (psArray *input, int scale);
+
+// utility functions:
+bool              psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip, psArray *rawstars, psArray *refstars);
+bool              psastroWriteStars (char *filename, psArray *sources);
+bool              psastroWriteTransform (psPlaneTransform *map);
+
+// mosaic fitting functions
+bool 		  psastroMosaicDistortion (pmFPA *fpa, psMetadata *recipe, int pass);
+psPlaneTransform *psastroMosaicFitRotAndScale (pmFPA *fpa);
+bool              psastroMosaicApplyRotAndScale (pmFPA *fpa, psPlaneTransform *TPtoFP);
+bool 		  psastroMosaicDistortionFromGradients (pmFPA *fpa, psMetadata *recipe);
+bool              psastroMosaicCorrectDistortion (pmFPA *fpa, psPlaneTransform *TPtoFP);
+bool 		  psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe);
+bool 		  psastroMosaicAstrom (pmConfig *config);
+bool 		  psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool 		  psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration);
+bool 		  psastroMosaicSetAstrom (pmFPA *fpa);
+bool 		  psastroMosaicHeaders (pmConfig *config);
+bool 		  psastroMosaicRescaleChips (pmFPA *fpa);
+bool 		  psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration);
+
+// Return version strings.
+psString 	  psastroVersion(void);
+psString 	  psastroVersionLong(void);
+
+// demo plots
+bool 		  psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe);
+bool 		  psastroPlotRefstars (psArray *refstars, psMetadata *recipe);
+bool 		  psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe);
+
+bool 		  psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip);
+bool 		  psastroDumpRefstars (psArray *refstars, char *filename);
+bool 		  psastroDumpMatches (pmFPA *fpa, char *filename);
+bool 		  psastroDumpStars (psArray *stars, char *filename);
+bool              psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match);
+bool              psastroDumpGradients (psArray *gradients, char *filename);
+
+bool		  psastroMosaicSetAstrom_tmp (pmFPA *fpa);
+int 		  psastroSortByMag (const void *a, const void *b);
+
+bool              psastroFixChips (pmConfig *config, psMetadata *recipe);
+bool              psastroFixChipsTest (pmConfig *config, psMetadata *recipe);
+bool              psastroUseModel (pmConfig *config, psMetadata *recipe);
+bool              psastroDumpCorners (char *filenameU, char *filenameD, pmFPA *fpa);
+
+
+psArray          *psastroReadGetstarCatalog (psFits *fits);
+psArray          *psastroReadGetstar_PS1_DEV_0 (psFits *fits);
+
+bool              psastroAstromGuessSetChip (pmFPA *fpa, pmChip *chip, const pmFPAview *view, double pixelScale, bool bilevelAstrometry);
+bool              psastroAstromGuessSetFPA (pmFPA *fpa, bool *bilevelAstrometry);
+bool              psastroMetadataStats (pmConfig *config);
+
+bool psastroZeroPointReadout(psArray *rawstars, psArray *refstars, psArray *matches);
+bool psastroZeroPoint (pmConfig *config);
+
+# endif /* PSASTRO_H */
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAnalysis.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAnalysis.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAnalysis.c	(revision 22306)
@@ -0,0 +1,90 @@
+# include "psastroInternal.h"
+
+bool psastroAnalysis (pmConfig *config) {
+
+    bool status;
+    int nStars;
+
+    // measure the total elapsed time in psastroAnalysis.
+    psTimerStart ("psastroAnalysis");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    if (!psastroUseModel (config, recipe)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to set model astrometry\n");
+        return false;
+    }
+
+    // interpret the available initial astrometric information
+    // apply the initial guess
+    if (!psastroAstromGuess (&nStars, config)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to determine initial astrometry guess\n");
+        return false;
+    }
+    if (nStars == 0) {
+        psLogMsg ("psastro", 2, "skipping astrometry analysis : no stars\n");
+        return false;
+    }
+
+    // load the reference stars overlapping the data stars
+    psArray *refs = psastroLoadRefstars(config);
+    if (!refs) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to load reference data\n");
+        return false;
+    }
+    if (refs->n == 0) {
+        psError(PSASTRO_ERR_REFSTARS, true, "no reference stars found");
+        psFree(refs);
+        return false;
+    }
+
+    if (!psastroChooseRefstars (config, refs)) {
+        psError (PSASTRO_ERR_UNKNOWN, false, "failed to select reference data for chips\n");
+        psFree(refs);
+        return false;
+    }
+
+    // check the command-line arguments first
+    bool chipastro = psMetadataLookupBool (&status, config->arguments, "PSASTRO.CHIP.MODE");
+    if (!status) {
+        chipastro = psMetadataLookupBool (&status, recipe, "PSASTRO.CHIP.MODE");
+    }
+    bool mosastro  = psMetadataLookupBool (&status, config->arguments, "PSASTRO.MOSAIC.MODE");
+    if (!status) {
+        mosastro  = psMetadataLookupBool (&status, recipe, "PSASTRO.MOSAIC.MODE");
+    }
+    if (!chipastro && !mosastro) {
+        psLogMsg ("psastro", 3, "no astrometry mode selected, assuming chip astrometry\n");
+        chipastro = true;
+    }
+
+    if (chipastro) {
+        if (!psastroChipAstrom (config)) {
+            psError (PSASTRO_ERR_UNKNOWN, false, "failed to perform single chip astrometry\n");
+            psFree(refs);
+            return false;
+        }
+    }
+    if (mosastro) {
+        if (!psastroMosaicAstrom (config)) {
+            psError (PSASTRO_ERR_UNKNOWN, false, "failed to perform mosaic camera astrometry\n");
+            psFree(refs);
+            return false;
+        }
+    }
+
+    // psastroZeroPoint (config);
+
+    psastroAstromGuessCheck (config);
+
+    // XXX how do we specify stack astrometry?
+    // psastroStackAstrom (config, refs);
+
+    psFree (refs);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroArguments.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroArguments.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroArguments.c	(revision 22306)
@@ -0,0 +1,108 @@
+# include "psastroStandAlone.h"
+
+pmConfig *psastroArguments (int argc, char **argv) {
+
+    bool status;
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // photcode : used in output to supplement header data (argument or recipe?)
+    if ((N = psArgumentGet (argc, argv, "-photcode"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+    
+    // apply the chip correction based on the reference astrometry?
+    if ((N = psArgumentGet (argc, argv, "-fixchips"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.FIX.CHIPS", PS_META_REPLACE, "", true);
+    }
+    // no valid header WCS: supply from astrom model
+    if ((N = psArgumentGet (argc, argv, "-use-astrommodel"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.USE.MODEL", PS_META_REPLACE, "", true);
+    }
+    // define the reference astrometry file
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "ASTROM.MODEL", "-astrommodel", "-astrommodellist");
+    if (status) {
+	// if supplied, assume -fixchips is desired
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.FIX.CHIPS", PS_META_REPLACE, "", true);
+    }
+
+    if ((N = psArgumentGet(argc, argv, "-stats"))) {
+        psArgumentRemove(N, &argc, argv);
+        psMetadataAddStr(config->arguments, PS_LIST_TAIL, "STATS", PS_META_REPLACE, "Filename for summary statistics", argv[N]);
+        psArgumentRemove(N, &argc, argv);
+    }
+
+    // apply mosastro mode?
+    if ((N = psArgumentGet (argc, argv, "-mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+mosastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.MOSAIC.MODE", PS_META_REPLACE, "", true);
+    }
+
+    // apply chipastro mode?
+    if ((N = psArgumentGet (argc, argv, "-chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", false);
+    }
+    if ((N = psArgumentGet (argc, argv, "+chipastro"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddBool (config->arguments, PS_LIST_TAIL, "PSASTRO.CHIP.MODE", PS_META_REPLACE, "", true);
+    }
+
+    // dump the configuration to a file?
+    if ((N = psArgumentGet (argc, argv, "-dumpconfig"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "DUMP_CONFIG", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -file (input) or -list (input)");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    if (argc != 2) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+        return NULL;
+    }
+
+    // output positions is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psastro", 1, "Done with psastroArguments...\n");
+    return (config);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAstromGuess.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAstromGuess.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroAstromGuess.c	(revision 22306)
@@ -0,0 +1,386 @@
+# include "psastroInternal.h"
+# define DEBUG 0
+
+// this function loads the header WCS astrometry terms into the fpa terms and applies the
+// astrometry to the detected objects.
+
+// this function assumes the initial astrometry arrives in the form of WCS keywords in the
+// headers corresponding to the chips.
+
+bool psastroAstromGuess (int *nStars, pmConfig *config) {
+
+    bool newFPA = true;
+    bool status = false;
+    double RAmin  = +FLT_MAX;
+    double RAmax  = -FLT_MAX;
+    double DECmin = +FLT_MAX;
+    double DECmax = -FLT_MAX;
+
+    double RAminSky = NAN;
+    double RAmaxSky = NAN;
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    *nStars = 0;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!");
+	return false;
+    }
+
+    // have we already supplied the astrometry from the model?
+    bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+    if (!status) {
+	useModel = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    psVector *cornerL = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerM = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerP = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQ = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerR = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerD = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    pmFPA *fpa = input->fpa;
+
+    if (DEBUG) psastroDumpCorners ("corners.up.guess1.dat", "corners.dn.guess1.dat", fpa);
+
+    // load mosaic-level astrometry?
+    bool bilevelAstrometry = false;
+    if (!useModel) {
+	psastroAstromGuessSetFPA (fpa, &bilevelAstrometry);
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	if (!useModel) {
+	    if (!psastroAstromGuessSetChip (fpa, chip, view, pixelScale, bilevelAstrometry)) continue;
+	}
+
+        if (newFPA) {
+            newFPA = false;
+	    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;
+            RAminSky = fpa->toSky->R - M_PI;
+            RAmaxSky = fpa->toSky->R + M_PI;
+        }
+
+	// report and save the current best guess for the chip 0,0 pixel coordinates
+	{ 
+	    psPlane ptCH, ptFP, ptTP;
+	    psSphere ptSky;
+
+	    ptCH.x = 0;
+	    ptCH.y = 0;
+	    psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+	    psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	    psDeproject (&ptSky, &ptTP, fpa->toSky);
+	    psLogMsg ("psastro", 3, "0,0 pix for chip %3d = %f,%f\n", view->chip, DEG_RAD*ptSky.r, DEG_RAD*ptSky.d);
+
+	    psVectorAppend (cornerL, ptFP.x);
+	    psVectorAppend (cornerM, ptFP.y);
+	    psVectorAppend (cornerP, ptTP.x);
+	    psVectorAppend (cornerQ, ptTP.y);
+	    psVectorAppend (cornerR, ptSky.r);
+	    psVectorAppend (cornerD, ptSky.d);
+	}
+
+        // apply the new WCS guess data to all of the data in the readouts
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+                if (rawstars == NULL) { continue; }
+
+		*nStars += rawstars->n;
+                for (int i = 0; i < rawstars->n; i++) {
+                    pmAstromObj *raw = rawstars->data[i];
+
+                    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+                    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+                    psDeproject (raw->sky, raw->TP, fpa->toSky);
+
+                    // rationalize ra to sky range centered on boresite
+                    while (raw->sky->r < RAminSky) raw->sky->r += 2.0*M_PI;
+                    while (raw->sky->r > RAmaxSky) raw->sky->r -= 2.0*M_PI;
+
+                    RAmin = PS_MIN (raw->sky->r, RAmin);
+                    RAmax = PS_MAX (raw->sky->r, RAmax);
+
+                    DECmin = PS_MIN (raw->sky->d, DECmin);
+                    DECmax = PS_MAX (raw->sky->d, DECmax);
+                }
+
+		// dump or plot the resulting projected positions
+		if (psTraceGetLevel("psastro.dump") > 0) {
+		    psastroDumpRawstars (rawstars, fpa, chip);
+		}
+
+		if (psTraceGetLevel("psastro.plot") > 0) {
+		    psastroPlotRawstars (rawstars, fpa, chip, recipe);
+		}
+            }
+        }
+    }
+
+    if (DEBUG) psastroDumpCorners ("corners.up.guess2.dat", "corners.dn.guess2.dat", fpa);
+
+    // how many total sources are available to us?
+    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NTOTSTAR",  PS_META_REPLACE, "", *nStars);
+    if (*nStars == 0) {
+	psLogMsg ("psastro", 2, "no sources available for astrometry\n");
+	psFree (view);
+	return true;
+    }
+
+    psLogMsg ("psastro", 2, "loaded raw data from %f,%f to %f,%f\n", 
+	      DEG_RAD*RAmin, DEG_RAD*DECmin, 
+	      DEG_RAD*RAmax, DEG_RAD*DECmax);
+
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MIN",  PS_META_REPLACE, "", RAmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "RA_MAX",  PS_META_REPLACE, "", RAmax);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MIN", PS_META_REPLACE, "", DECmin);
+    psMetadataAddF32 (recipe, PS_LIST_TAIL, "DEC_MAX", PS_META_REPLACE, "", DECmax);
+
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.L", PS_META_REPLACE, "corner pixel", cornerL);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.M", PS_META_REPLACE, "corner pixel", cornerM);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.P", PS_META_REPLACE, "corner pixel", cornerP);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.Q", PS_META_REPLACE, "corner pixel", cornerQ);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.R", PS_META_REPLACE, "corner pixel", cornerR);
+    psMetadataAddVector (input->fpa->analysis, PS_LIST_TAIL, "CORNER.D", PS_META_REPLACE, "corner pixel", cornerD);
+
+    psFree (cornerL);
+    psFree (cornerM);
+    psFree (cornerP);
+    psFree (cornerQ);
+    psFree (cornerR);
+    psFree (cornerD);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
+
+bool psastroAstromGuessSetChip (pmFPA *fpa, pmChip *chip, const pmFPAview *view, double pixelScale, bool bilevelAstrometry) {
+
+    // read WCS data from the corresponding header
+    pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+    if (bilevelAstrometry) {
+	if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+	    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+	    return false;
+	} 
+    } else {
+	if (!pmAstromReadWCS (fpa, chip, hdu->header, pixelScale)) {
+	    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+	    return false;
+	} 
+    }
+    return true;
+}
+
+bool psastroAstromGuessSetFPA (pmFPA *fpa, bool *bilevelAstrometry) {
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmHDU *phu = pmFPAviewThisPHU (view, fpa);
+
+    *bilevelAstrometry = false;
+
+    // load mosaic-level astrometry?
+    if (phu) {
+	char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+	if (ctype) {
+	    *bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+	}
+    }
+    if (*bilevelAstrometry) {
+	pmAstromReadBilevelMosaic (fpa, phu->header);
+    } 
+    psFree (view);
+    return true;
+}
+
+// we made a guess at the beginning; how does the guess compare with the result?
+bool psastroAstromGuessCheck (pmConfig *config) {
+
+    bool status;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    pmFPA *fpa = input->fpa;
+
+    psVector *cornerLo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.L");
+    psVector *cornerMo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.M");
+    psVector *cornerPo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.P");
+    psVector *cornerQo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.Q");
+    psVector *cornerRo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.R");
+    psVector *cornerDo = psMetadataLookupPtr (&status, input->fpa->analysis, "CORNER.D");
+
+    if (cornerLo->n < 3) return true;
+
+    psVector *cornerLn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerMn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerPn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerRn = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerDn = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    if (DEBUG) psastroDumpCorners ("corners.up.guess3.dat", "corners.dn.guess3.dat", fpa);
+
+    pmChip *chip = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	psPlane ptCH, ptFP, ptTP;
+	psSphere ptSky;
+
+	ptCH.x = 0;
+	ptCH.y = 0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCH);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	psLogMsg ("psastro", 3, "0,0 pix for chip %3d = %f,%f\n", view->chip, DEG_RAD*ptSky.r, DEG_RAD*ptSky.d);
+
+	// new corner locations based on the calibrated astrometry
+	psVectorAppend (cornerLn, ptFP.x);
+	psVectorAppend (cornerMn, ptFP.y);
+	psVectorAppend (cornerPn, ptTP.x);
+	psVectorAppend (cornerQn, ptTP.y);
+	psVectorAppend (cornerRn, ptSky.r);
+	psVectorAppend (cornerDn, ptSky.d);
+    }
+
+    // compare the old R,D values projected to the same tangent plane as the new R,D values:
+
+    psVector *cornerPs = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *cornerQs = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    for (int i = 0; i < cornerRo->n; i++) {
+	
+	psPlane ptTP;
+	psSphere ptSky;
+
+	ptSky.r = cornerRo->data.F32[i];
+	ptSky.d = cornerDo->data.F32[i];
+
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psVectorAppend (cornerPs, ptTP.x);
+	psVectorAppend (cornerQs, ptTP.y);
+    }
+
+    psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+    map->x->coeffMask[1][1] = PS_POLY_MASK_SET;
+    map->y->coeffMask[1][1] = PS_POLY_MASK_SET;
+    
+    psVectorFitPolynomial2D (map->x, NULL, 0, cornerPn, NULL, cornerPs, cornerQs);
+    psVectorFitPolynomial2D (map->y, NULL, 0, cornerQn, NULL, cornerPs, cornerQs);
+    
+    // apply the linear fit...
+    psVector *cornerPf = psPolynomial2DEvalVector (map->x, cornerPs, cornerQs);
+    psVector *cornerQf = psPolynomial2DEvalVector (map->y, cornerPs, cornerQs);
+
+    // ...and calculate the residual between Pn,Qn and Pf,Qf
+    psVector *cornerPd = (psVector *) psBinaryOp (NULL, cornerPn, "-", cornerPf);
+    psVector *cornerQd = (psVector *) psBinaryOp (NULL, cornerQn, "-", cornerQf);
+
+    psStats *statsP = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+    psStats *statsQ = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV);
+
+    psVectorStats (statsP, cornerPd, NULL, NULL, 0);
+    psVectorStats (statsQ, cornerQd, NULL, NULL, 0);
+
+    float angle = atan2 (map->y->coeff[1][0], map->x->coeff[1][0]);
+    float scale = hypot (map->y->coeff[1][0], map->x->coeff[1][0]);
+    
+    psLogMsg ("psastro", 3, "boresite offset  : %f,%f\n", map->x->coeff[0][0], map->y->coeff[0][0]);
+    psLogMsg ("psastro", 3, "boresite angle   : %f, scale: %f", angle*PS_DEG_RAD, scale);
+    psLogMsg ("psastro", 3, "boresite scatter : %f,%f\n", statsP->sampleStdev, statsQ->sampleStdev);
+
+    // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+    psMetadata *header = psMetadataLookupMetadata (&status, input->fpa->analysis, "PSASTRO.HEADER");
+    if (!header) {
+	header = psMetadataAlloc();
+	psMetadataAddMetadata (input->fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", header);
+	psFree (header);  // drop this reference
+    }
+
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_R0", PS_META_REPLACE, "boresite offset in RA (TP units)", map->x->coeff[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_D0", PS_META_REPLACE, "boresite offset in DEC (TP units)", map->y->coeff[0][0]);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_T0", PS_META_REPLACE, "boresite angle (degrees)", angle*PS_DEG_RAD);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_S0", PS_META_REPLACE, "boresite scale correction", scale);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_RS", PS_META_REPLACE, "boresite scatter in RA (TP units)", statsP->sampleStdev);
+    psMetadataAddF32 (header, PS_LIST_TAIL, "AST_DS", PS_META_REPLACE, "boresite scatter in DEC (TP units)", statsQ->sampleStdev);
+
+    if (0) {
+	FILE *f = fopen ("corners.dat", "w");
+	for (int i = 0; i < cornerRo->n; i++) {
+	    fprintf (f, "%10.6f %10.6f  %9.2f %9.2f  %9.2f %9.2f  |  %10.6f %10.6f  %9.2f %9.2f  %9.2f %9.2f\n",
+		     cornerRn->data.F32[i], cornerDn->data.F32[i], cornerPn->data.F32[i], cornerQn->data.F32[i], cornerLn->data.F32[i], cornerMn->data.F32[i], 
+		     cornerRo->data.F32[i], cornerDo->data.F32[i], cornerPo->data.F32[i], cornerQo->data.F32[i], cornerLo->data.F32[i], cornerMo->data.F32[i]);
+	}
+	fclose (f);
+    }
+
+    psFree (cornerPf);
+    psFree (cornerQf);
+    psFree (cornerPd);
+    psFree (cornerQd);
+
+    psFree (statsP);
+    psFree (statsQ);
+
+    psFree (cornerLn);
+    psFree (cornerMn);
+    psFree (cornerPn);
+    psFree (cornerQn);
+    psFree (cornerRn);
+    psFree (cornerDn);
+    psFree (cornerPs);
+    psFree (cornerQs);
+    psFree (map);
+    psFree (view);
+    
+
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChipAstrom.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChipAstrom.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChipAstrom.c	(revision 22306)
@@ -0,0 +1,133 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroChipAstrom (pmConfig *config) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    int numGoodChips = 0;               // Number of chips for which astrometry succeeds
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        int numGoodCells = 0;           // Number of cells for which astrometry succeeds
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+            if (!chip->fromFPA) { continue; }
+
+            // process each of the readouts
+            int numGoodRO = 0;          // Number of readouts for which astrometry succeeds
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // select the raw objects for this readout
+                psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+                if (rawstars == NULL) { continue; }
+
+                // select the raw objects for this readout
+                psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+                if (refstars == NULL) { continue; }
+
+                // the absolute minimum number of stars is 4 (for order = 1)
+                if ((rawstars->n < 4) || (refstars->n < 4)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)",
+                              rawstars->n, refstars->n);
+                    continue;
+                }
+
+                // save WCS and analysis metadata in update header
+                psMetadata *updates = psMetadataAlloc();
+
+                // XXX update the header with info to reflect the failure
+                if (!psastroOneChipGrid (fpa, chip, refstars, rawstars, recipe, updates)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "failed to find a solution\n");
+                    psFree (updates);
+                    continue;
+                }
+                // XXX update the header with info to reflect the failure
+                if (!psastroOneChipFit (fpa, chip, refstars, rawstars, recipe, updates)) {
+                    readout->data_exists = false;
+                    psLogMsg ("psastro", 3, "failed to find a solution\n");
+                    psFree (updates);
+                    continue;
+                }
+
+                numGoodRO++;
+
+                // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+                psMetadataAddF32 (updates, PS_LIST_TAIL, "DT_ASTR", PS_META_REPLACE, "elapsed psastro time", psTimerMark ("psastroAnalysis"));
+
+                pmAstromWriteWCS (updates, fpa, chip, NONLIN_TOL);
+                psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_DATA_METADATA, "psastro header stats", updates);
+                psFree (updates);
+
+                if (psTraceGetLevel("psastro.dump") > 0) {
+
+                    char *filename = NULL;
+                    char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+                    psStringAppend (&filename, "rawstars.ch.%s.dat", chipname);
+                    psastroDumpStars (rawstars, filename);
+                    psFree (filename);
+                    filename = NULL;
+
+                    psStringAppend (&filename, "refstars.ch.%s.dat", chipname);
+                    psastroDumpStars (refstars, filename);
+                    psFree (filename);
+                    filename = NULL;
+                }
+            }
+            if (numGoodRO > 0) {
+                numGoodCells++;
+            }
+        }
+        if (numGoodCells > 0) {
+            numGoodChips++;
+        }
+    }
+
+    if (fpa->chips->n == 1 && numGoodChips == 0) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "Failed to fit single chip.");
+        return false;
+    }
+
+    if (!psastroFixChips (config, recipe)) {
+        psError(PSASTRO_ERR_UNKNOWN, false, "failed to align problematic chips");
+        return false;
+    }
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChooseRefstars.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChooseRefstars.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroChooseRefstars.c	(revision 22306)
@@ -0,0 +1,133 @@
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroChooseRefstars"); \
+  psFree (index); \
+  psFree (view); \
+  return false; \
+}
+
+bool psastroChooseRefstars (pmConfig *config, psArray *refs) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+        return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+        psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+        return false;
+    }
+
+    // extra field fraction to add
+    double fieldPadding = psMetadataLookupF32 (&status, recipe, "PSASTRO.FIELD.PADDING");
+    if (!status) fieldPadding = 0.0;
+
+    bool matchLumFunc = psMetadataLookupBool (&status, recipe, "PSASTRO.MATCH.LUMFUNC");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    // sort by mag
+    psVector *index = psArraySortIndex (NULL, refs, psastroSortByMag);
+
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NREF");
+
+    // de-activate all files except PSASTRO.REFSTARS
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUT.REFSTARS");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->fromFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip in astrometry, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+		psRegion *extent = pmReadoutExtent (readout);
+		if (!extent) {
+		    psError(PSASTRO_ERR_CONFIG, true, "Can't find readout size!\n");
+		    return NULL;
+		}
+
+		int Nx = abs(extent->x1 - extent->x0);
+		int Ny = abs(extent->y1 - extent->y0);
+
+                float minX = -fieldPadding*Nx;
+                float maxX = (1+fieldPadding)*Nx;
+                float minY = -fieldPadding*Ny;
+                float maxY = (1+fieldPadding)*Ny;
+
+                // the refstars is a subset within range of this chip
+                psArray *refstars = psArrayAllocEmpty (100);
+
+                // select the reference objects within range of this readout
+                // project the reference objects to this chip
+                for (int i = 0; i < refs->n; i++) {
+                    pmAstromObj *ref = pmAstromObjCopy(refs->data[index->data.S32[i]]);
+
+                    psProject (ref->TP, ref->sky, fpa->toSky);
+                    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+                    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+
+                    // limit the X,Y range of the refs to the selected chip
+                    if (ref->chip->x < minX) goto skip;
+                    if (ref->chip->x > maxX) goto skip;
+                    if (ref->chip->y < minY) goto skip;
+                    if (ref->chip->y > maxY) goto skip;
+
+                    psArrayAdd (refstars, 100, ref);
+                skip:
+                    psFree (ref);
+
+		    if (nMax && (refstars->n >= nMax)) break;
+                }
+                psTrace ("psastro", 4, "Added %ld refstars\n", refstars->n);
+
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS", PS_DATA_ARRAY, "astrometry matches", refstars);
+		psFree (refstars);
+		psFree (extent);
+
+		if (matchLumFunc) {
+		    // in this case, no PSASTRO.REFSTARS is added to readout->analysis
+		    if (!psastroRefstarSubset (readout)) {
+			psError(PSASTRO_ERR_DATA, false, "Can't determine an appropriate refstar subset\n");
+			psFree (index);
+			psFree (view);
+			return false;
+		    }
+		}
+            }
+        }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // activate all files except PSASTRO.OUTPUT
+    pmFPAfileActivate (config->files, true, NULL);
+    pmFPAfileActivate (config->files, false, "PSASTRO.OUT.REFSTARS");
+
+    bool onlyRefstars = psMetadataLookupBool (&status, recipe, "PSASTRO.ONLY.REFSTARS");
+    if (onlyRefstars) exit (0);
+
+    psFree (index);
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroCleanup.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroCleanup.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroCleanup.c	(revision 22306)
@@ -0,0 +1,17 @@
+# include "psastroStandAlone.h"
+
+void psastroCleanup (pmConfig *config) {
+
+    psFree (config);
+
+    psTimerStop ();
+    psMemCheckCorruption (stderr, true);
+    pmModelClassCleanup ();
+    psTimeFinalize ();
+    pmConceptsDone ();
+    pmConfigDone ();
+    fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, stdout, false), "psastro");
+    // fprintf (stderr, "found %d leaks at %s\n", psMemCheckLeaks (0, NULL, NULL, false), "psastro");
+
+    return;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroConvert.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroConvert.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroConvert.c	(revision 22306)
@@ -0,0 +1,173 @@
+# include "psastroInternal.h"
+// leak free 2006.04.27
+
+bool psastroConvertFPA (pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                psastroConvertReadout (readout, recipe);
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
+
+// pass/apply the WCS information?
+bool psastroConvertReadout (pmReadout *readout, psMetadata *recipe) {
+
+    bool status;
+
+    // PSPHOT.SOURCES carries the pmSource objects (from psphot analysis or loaded externally)
+    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    if (sources == NULL)
+        return false;
+
+    // convert the pmSource objects into pmAstromObj objects (drop !STAR and SATSTAR?)
+    psArray *inStars = pmSourceToAstromObj (sources);
+
+    // sort in ascending magnitude order
+    // psArraySort (inStars, psastroSortByMag);
+    // psVector *index = psArraySortIndex (sources, pmSourceSortBySN);
+    psVector *index = psArraySortIndex (NULL, inStars, psastroSortByMag);
+
+    // XXX need to exit gracefully is inStars->n is 0 (or 1?)
+
+    // we are going to select the brighter Nmax subset for astrometry 
+    int nMax = psMetadataLookupS32 (&status, recipe, "PSASTRO.MAX.NRAW");
+    if (!status || !nMax) nMax = inStars->n;
+
+    // we are going to select the brighter Nmax subset for astrometry 
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.MAX.INST.MAG.RAW");
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.MIN.INST.MAG.RAW");
+
+    // we are going to select the brighter Nmax subset for astrometry 
+    pmSourceMode skip = PM_SOURCE_MODE_DEFAULT;
+    char *ignoreList = psMetadataLookupStr (&status, recipe, "PSASTRO.IGNORE");
+    if (ignoreList != NULL) {
+      psArray *list = psStringSplitArray (ignoreList, ",", false);
+      for (int i = 0; i < list->n; i++) {
+	pmSourceMode mode = pmSourceModeFromString (list->data[i]);
+	if (mode == PM_SOURCE_MODE_DEFAULT) {
+	  psWarning ("unknown source mode in PSASTRO.IGNORE, skipping");
+	  continue;
+	}
+	skip |= mode;
+      }
+      psFree (list);
+    }
+
+    // choose the first nMax sources
+    int j = 0;
+    psArray *rawStars = psArrayAlloc (PS_MIN (nMax, inStars->n));
+
+    float mMin = +100.0;
+    float mMax = -100.0;
+    int nModeSkip = 0;
+    int nFaintSkip = 0;
+    int nBrightSkip = 0;
+    int nInfSkip = 0;
+
+    for (int i = 0; (i < inStars->n) && (j < rawStars->n); i++) {
+	int n = index->data.S32[i];
+	pmSource *source = sources->data[n];
+	// XXX this needs to be flexible
+	if (source->mode & skip) { 
+	  nModeSkip ++;
+	  continue;
+	}
+	if ((iMagMax != 0.0) && (source->psfMag > iMagMax)) {
+	  nFaintSkip ++;
+	  continue;
+	}
+	if ((iMagMin != 0.0) && (source->psfMag < iMagMin)) {
+	  nBrightSkip ++;
+	  continue;
+	}
+	if (!isfinite(source->psfMag)) {
+	  nInfSkip ++;
+	  continue;
+	}
+	mMin = PS_MIN (mMin, source->psfMag);
+	mMax = PS_MAX (mMax, source->psfMag);
+	rawStars->data[j] = psMemIncrRefCounter (inStars->data[n]);
+	j++;
+    }
+    rawStars->n = j;
+
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.RAWSTARS", PS_DATA_ARRAY, "astrometry objects", rawStars);
+    psLogMsg ("psastro", 4, "loaded %ld sources, using %ld of %ld good sources (inst mag: %f to %f)\n", sources->n, rawStars->n, inStars->n, mMin, mMax);
+    psLogMsg ("psastro", 4, "skip reasons: mode: %d, faint: %d, bright: %d, inf: %d\n", nModeSkip, nFaintSkip, nBrightSkip, nInfSkip);
+
+    psFree (index);
+    psFree (inStars);
+    psFree (rawStars);
+
+    return true;
+}
+
+// select a magnitude range?
+psArray *pmSourceToAstromObj (psArray *sources) {
+
+    psArray *objects = psArrayAllocEmpty (sources->n);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // only accept the PSF sources?
+	// XXX drop SATSTAR?
+        // if (source->type != PM_SOURCE_TYPE_STAR) continue;
+
+        pmModel *model = source->modelPSF;
+        if (model == NULL) continue;
+
+        psF32 *PAR = model->params->data.F32;
+
+        pmAstromObj *obj = pmAstromObjAlloc ();
+
+        // is the source magnitude calibrated in any sense?
+        obj->pix->x = PAR[PM_PAR_XPOS];
+        obj->pix->y = PAR[PM_PAR_YPOS];
+        obj->Mag = source->psfMag;
+
+        // XXX do we have the information giving the readout and cell offset?
+        // for the moment, assume chip == cell == readout
+        *obj->cell = *obj->pix;
+        *obj->chip = *obj->cell;
+
+        psArrayAdd (objects, 100, obj);
+        psFree (obj);
+    }
+    return objects;
+}
+
+// sort by Mag (ascending)
+int psastroSortByMag (const void *a, const void *b)
+{
+    const pmAstromObj *A = a;
+    const pmAstromObj *B = b;
+
+    psF32 mA = (isfinite(A->Mag)) ? A->Mag : FLT_MAX;
+    psF32 mB = (isfinite(B->Mag)) ? B->Mag : FLT_MAX;
+
+    psF32 diff = mA - mB;
+    if (diff > +FLT_EPSILON) return (+1);
+    if (diff < -FLT_EPSILON) return (-1);
+    return (0);
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataLoad.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataLoad.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataLoad.c	(revision 22306)
@@ -0,0 +1,75 @@
+# include "psastroStandAlone.h"
+// this loop loads the data from the input files and selects the
+// brighter stars for astrometry
+// at the end of this function, the complete stellar data is loaded
+// into the correct fpa structure locations (readout.analysis:PSPHOT.SOURCES)
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataLoad"); \
+  psFree (view); \
+  return false; \
+}
+  
+// all of the different astrometry analysis modes use the same data load loop
+bool psastroDataLoad (pmConfig *config) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    // de-activate all files except PSASTRO.INPUT
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.INPUT");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+		if (!readout->data_exists) { continue; }
+
+		if (!psastroConvertReadout (readout, recipe)) ESCAPE;
+
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	}
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "load data : %f sec\n", psTimerMark ("psastro"));
+
+    psFree (view);
+    return true;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataSave.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataSave.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDataSave.c	(revision 22306)
@@ -0,0 +1,72 @@
+# include "psastroInternal.h"
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+// this loop saves the photometry/astrometry data files
+bool psastroDataSave (pmConfig *config) {
+
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the output data sources
+    pmFPAfile *output = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUTPUT");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find or interpret output file rule PSASTRO.OUTPUT!\n");
+	return false;
+    }
+
+    // de-activate all files except PSASTRO.OUTPUT
+    pmFPAfileActivate (config->files, false, NULL);
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUTPUT");
+    pmFPAfileActivate (config->files, true, "PSASTRO.OUT.ASTROM");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // open/load files as needed
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	while ((cell = pmFPAviewNextCell (view, output->fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+	    // process each of the readouts
+	    while ((readout = pmFPAviewNextReadout (view, output->fpa, 1)) != NULL) {
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+		if (!readout->data_exists) { continue; }
+
+		if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	    }
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+	}
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    // Write out summary statistics
+    if (!psastroMetadataStats (config)) ESCAPE;
+
+    // activate all files except PSASTRO.OUTPUT
+    pmFPAfileActivate (config->files, true, NULL);
+    pmFPAfileActivate (config->files, false, "PSASTRO.OUTPUT");
+
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDefineFiles.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDefineFiles.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDefineFiles.c	(revision 22306)
@@ -0,0 +1,117 @@
+# include "psastroInternal.h"
+
+bool psastroDefineFiles (pmConfig *config, pmFPAfile *input) {
+
+    // these calls bind the I/O handle to the specified fpa
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUTPUT");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+    output->save = true;
+
+    bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+    if (!status) {
+	fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+    }
+    bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+    if (!status) {
+	fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+    }
+    if (fixChips || useModel) {
+        if (!psastroDefineFile (config, input->fpa, "PSASTRO.MODEL", "ASTROM.MODEL", PM_FPA_FILE_ASTROM_MODEL, PM_DETREND_TYPE_ASTROM)) {
+            psError (PS_ERR_IO, false, "Can't find an astrometry model file");
+            return NULL;
+        }
+    }
+
+    bool saveRefstars = psMetadataLookupBool (&status, config->arguments, "PSASTRO.SAVE.REFSTARS");
+    if (!status) {
+      saveRefstars = psMetadataLookupBool (&status, recipe, "PSASTRO.SAVE.REFSTARS");
+    }
+    if (saveRefstars) {
+	// look for the file in the camera config table
+	pmFPAfile *file = pmFPAfileDefineOutputFromFile  (config, input, "PSASTRO.OUT.REFSTARS");
+	if (!file) {
+	    psError (PS_ERR_IO, false, "Can't find the astrometry refstars file definition");
+	    return NULL;
+	}
+	if (file->type != PM_FPA_FILE_ASTROM_REFSTARS) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", "PSASTRO.OUT.REFSTARS", pmFPAfileStringFromType (PM_FPA_FILE_ASTROM_REFSTARS));
+	    return NULL;
+	}
+	file->save = true;
+    }
+
+
+# if (0)
+    // optionally save output plots
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.MOMENTS")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.MOMENTS");
+    }
+    if (!pmFPAfileDefineOutput (config, input->fpa, "SOURCE.PLOT.PSFMODEL")) {
+	psTrace ("psphot", 3, "Cannot find a rule for SOURCE.PLOT.PSFMODEL");
+    }
+# endif
+
+    return true;
+}
+
+bool psastroDefineFile (pmConfig *config, pmFPA *input, char *filerule, char *argname, pmFPAfileType fileType, pmDetrendType detrendType) {
+
+    bool status;
+    pmFPAfile *file;
+
+    // look for the file on the argument list
+    file = pmFPAfileDefineFromArgs  (&status, config, filerule, argname);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+
+    // look for the file in the camera config table
+    file = pmFPAfileDefineFromConf  (&status, config, filerule);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load find definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+
+    // look for the file to be loaded from the detrend database
+    file = pmFPAfileDefineFromDetDB (&status, config, filerule, input, detrendType);
+    if (!status) {
+	psError (PS_ERR_UNKNOWN, false, "failed to load file definition");
+	return false;
+    }
+    if (file) {
+	if (file->type != fileType) {
+	    psError(PS_ERR_IO, true, "%s is not of type %s", filerule, pmFPAfileStringFromType (fileType));
+	    return false;
+	}
+	return true;
+    }
+    return false;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoDump.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoDump.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoDump.c	(revision 22306)
@@ -0,0 +1,297 @@
+# include "psastroInternal.h"
+
+// this function is used for test purposes (-trace psastro.dump.psastroAstromGuess 1)
+bool psastroDumpStars (psArray *stars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < stars->n; i++) {
+	pmAstromObj *obj = stars->data[i];
+
+	// write out the upward projections
+	fprintf (f, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 obj->sky->r, obj->sky->d, obj->Mag, 
+		 obj->TP->x, obj->TP->y, 
+		 obj->FP->x, obj->FP->y, 
+		 obj->chip->x, obj->chip->y);
+    }
+    fclose (f);
+    return true;
+}
+
+// this function is used for test purposes (-trace psastro.dump.psastroAstromGuess 1)
+bool psastroDumpRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip) {
+
+    char *filename = NULL;
+    char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+
+    psStringAppend (&filename, "rawstars.up.%s.dat", chipname);
+    FILE *f1 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    psStringAppend (&filename, "rawstars.dn.%s.dat", chipname);
+    FILE *f2 = fopen (filename, "w");
+    psFree (filename);
+    filename = NULL;
+
+    for (int i = 0; i < rawstars->n; i++) {
+	pmAstromObj *raw = rawstars->data[i];
+
+	psPlane *fp = psPlaneAlloc();
+	psPlane *tp = psPlaneAlloc();
+	psPlane *ch = psPlaneAlloc();
+			
+	psProject (tp, raw->sky, fpa->toSky);
+	psPlaneTransformApply (fp, fpa->fromTPA, tp);
+	psPlaneTransformApply (ch, chip->fromFPA, fp);
+			
+	// write out the upward projections
+	fprintf (f1, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y);
+		
+	// write out the downward projections
+	fprintf (f2, "%d  %f %f  %f  %f %f  %f %f  %f %f\n", i,
+		 raw->sky->r, raw->sky->d, raw->Mag, 
+		 tp->x, tp->y, 
+		 fp->x, fp->y, 
+		 ch->x, ch->y);
+		
+	psFree (fp);
+	psFree (tp);
+	psFree (ch);
+    }
+
+    fclose (f1);
+    fclose (f2);
+    return true;
+}
+
+bool psastroDumpMatchedStars (char *filename, psArray *rawstars, psArray *refstars, psArray *match) {
+    
+    FILE *f = fopen (filename, "w");
+
+    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];
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f   |   ",  
+		 DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+		 raw->TP->x, raw->TP->y, 
+		 raw->FP->x, raw->FP->y, 
+		 raw->chip->x, raw->chip->y, raw->Mag);
+
+	fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+		 DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+		 ref->TP->x, ref->TP->y, 
+		 ref->FP->x, ref->FP->y, 
+		 ref->chip->x, ref->chip->y, ref->Mag);
+    }
+    fclose (f);
+
+    return true;
+}
+
+// this function is used for test purposes (-trace psastro.dump.psastroLoadRefstars 1)
+bool psastroDumpRefstars (psArray *refstars, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < refstars->n; i++) {
+	pmAstromObj *ref = refstars->data[i];
+
+	// write out the refstar data
+	fprintf (f, "%d  %f %f  %f\n", i,
+		 ref->sky->r, ref->sky->d, ref->Mag);
+    }
+
+    fclose (f);
+    return true;
+}
+
+bool psastroDumpMatches (pmFPA *fpa, char *filename) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    FILE *f = fopen (filename, "w");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) continue;
+	
+	char *chipName = psMetadataLookupStr(NULL, chip->concepts, "CHIP.NAME");
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) continue;
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) continue;
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) continue;
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) continue;
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (matches == NULL) continue;
+
+		for (int i = 0; i < matches->n; i++) {
+		    pmAstromMatch *match = matches->data[i];
+
+		    pmAstromObj *raw = rawstars->data[match->raw];
+		    fprintf (f, "%s  %f %f  %f %f  %f %f  %f %f  %f   |   ",  
+			     chipName, DEG_RAD*raw->sky->r, DEG_RAD*raw->sky->d, 
+			     raw->TP->x, raw->TP->y, 
+			     raw->FP->x, raw->FP->y, 
+			     raw->chip->x, raw->chip->y, raw->Mag);
+
+		    pmAstromObj *ref = refstars->data[match->ref];
+		    fprintf (f, "%f %f  %f %f  %f %f  %f %f  %f\n", 
+			     DEG_RAD*ref->sky->r, DEG_RAD*ref->sky->d, 
+			     ref->TP->x, ref->TP->y, 
+			     ref->FP->x, ref->FP->y, 
+			     ref->chip->x, ref->chip->y, ref->Mag);
+		}
+	    }
+	}
+    }
+    fclose (f);
+    psFree (view);
+    return true;
+}
+
+// this function is used for test purposes (-trace psastro.dump 1)
+bool psastroDumpGradients (psArray *gradients, char *filename) {
+
+    FILE *f = fopen (filename, "w");
+
+    for (int i = 0; i < gradients->n; i++) {
+	pmAstromGradient *gradient = gradients->data[i];
+
+	// write out the refstar data
+	fprintf (f, "%d  %f %f   %f %f  %f %f\n", i,
+		 gradient->FP.x, gradient->FP.y, 
+		 gradient->dTPdL.x, gradient->dTPdL.y, 
+		 gradient->dTPdM.x, gradient->dTPdM.y);
+    }
+
+    fclose (f);
+    return true;
+}
+
+bool psastroDumpCorners (char *filenameU, char *filenameD, pmFPA *fpa) {
+
+  // XXX test output of chip corners based on model
+  FILE *fu = fopen (filenameU, "w");
+  FILE *fd = fopen (filenameD, "w");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  float fpaAngle = PM_DEG_RAD * atan2 (fpa->toTPA->y->coeff[1][0], fpa->toTPA->x->coeff[1][0]);
+
+  fprintf (fu, "# boresite: %f, %f @ %f\n", fpa->toSky->R*PS_DEG_RAD, fpa->toSky->D*PS_DEG_RAD, fpaAngle);
+  fprintf (fd, "# boresite: %f, %f @ %f\n", fpa->toSky->R*PS_DEG_RAD, fpa->toSky->D*PS_DEG_RAD, fpaAngle);
+
+  pmChip *chip = NULL;
+  while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	// XXX write out the four corners for a test
+	psRegion *region = pmChipPixels (chip);
+	psPlane ptCP, ptFP, ptTP;
+	psSphere ptSky;
+
+	// UP 0,0
+	ptCP.x = region->x0; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 1,0
+	ptCP.x = region->x1; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 1,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 1,1
+	ptCP.x = region->x1; ptCP.y = region->y1;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 1,1
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 0,1
+	ptCP.x = region->x0; ptCP.y = region->y1;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,1
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// UP 0,0
+	ptCP.x = region->x0; ptCP.y = region->y0;
+	psPlaneTransformApply (&ptFP, chip->toFPA, &ptCP);
+	psPlaneTransformApply (&ptTP, fpa->toTPA, &ptFP);
+	psDeproject (&ptSky, &ptTP, fpa->toSky);
+	fprintf (fu, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	// DOWN 0,0
+	psProject (&ptTP, &ptSky, fpa->toSky);
+	psPlaneTransformApply (&ptFP, fpa->fromTPA, &ptTP);
+	psPlaneTransformApply (&ptCP, chip->fromFPA, &ptFP);
+	fprintf (fd, "%10.6f %10.6f  %8.1f %8.1f  %8.1f %8.1f  %8.1f %8.1f\n", ptSky.r, ptSky.d, ptTP.x, ptTP.y, ptFP.x, ptFP.y, ptCP.x, ptCP.y);
+
+	psFree (region);
+  }
+
+  fclose (fu);
+  fclose (fd);
+  psFree (view);
+  return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoPlot.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoPlot.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroDemoPlot.c	(revision 22306)
@@ -0,0 +1,514 @@
+# include "psastroInternal.h"
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing);
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float iMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MIN");
+    float iMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.INST.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    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);
+
+    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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    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;
+
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (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:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [rawstars.png]: ");
+        if (fscanf(stdin, "%s", name) != 1) {
+            psWarning("Unable to read plot name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "rawstars.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotRefstars (psArray *refstars, psMetadata *recipe)
+{
+    Graphdata graphdata;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    bool status = false;
+    float rMagMin = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MIN");
+    float rMagMax = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLOT.REF.MAG.MAX");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearSections (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (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:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Couldn't read anything.");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [refstars.png]: ");
+        if (fscanf (stdin, "%s", name) != 1) {
+            psWarning("Unable to read name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "refstars.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match, psMetadata *recipe) {
+
+    Graphdata graphdata;
+    KapaSection section;
+
+    int kapa = pmKapaOpen (true);
+    if (kapa == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    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");
+
+    KapaResize (kapa, 1000, 1000);
+    KapaInitGraph (&graphdata);
+    KapaClearPlots (kapa);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+
+    section.dx = 0.5;
+    section.dy = 0.5;
+
+    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 (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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa, &graphdata, xVec, yVec, zVec, false);
+
+    // *** X vs Y plot (different window)
+    int kapa2 = KapaOpenNamedSocket ("kapa", "XvsY");
+    if (kapa2 == -1) {
+        psError(PS_ERR_UNKNOWN, true, "failure to open kapa");
+        return false;
+    }
+
+    KapaResize (kapa2, 1000, 1000);
+    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;
+    pmKapaPlotVectorTriple_AutoLimits_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.style = 2;
+
+    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);
+
+    // X vs Y by mag (raw)
+    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;
+    pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (kapa2, &graphdata, xVec, yVec, zVec, false);
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10], name[80];
+    fprintf (stdout, "(s)ave plot or [c]ontinue? ");
+    if (!fgets (key, 8, stdin)) {
+        psWarning("Couldn't read anything");
+    } else if (key[0] == 's') {
+        fprintf (stdout, "enter plot name [chipfit.png]: ");
+        if (fscanf (stdin, "%s", name) != 1) {
+            psWarning("Couldn't read name");
+        } else if (!strcmp (name, "")) {
+            strcpy (name, "chipfit.png");
+        }
+        KapaPNG (kapa, name);
+    }
+
+    close (kapa2);
+    psFree (xVec);
+    psFree (yVec);
+    psFree (zVec);
+    return true;
+}
+
+bool pmKapaPlotVectorTriple_AutoLimitsZscale_OpenGraph (int kapa, Graphdata *graphdata, psVector *xVec, psVector *yVec, psVector *zVec, bool increasing)
+{
+
+    // set limits based on data values
+    float zmin = +FLT_MAX;
+    float zmax = -FLT_MAX;
+    for (int i = 0; i < xVec->n; i++) {
+        zmin = PS_MIN (zmin, zVec->data.F32[i]);
+        zmax = PS_MAX (zmax, zVec->data.F32[i]);
+    }
+
+    // set the scale vector
+    psVector *zScale = psVectorAlloc (zVec->n, PS_DATA_F32);
+
+    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));
+            }
+        }
+    }
+
+    KapaSetFont (kapa, "helvetica", 14);
+    KapaBox (kapa, graphdata);
+
+    // the point size will be scaled from the z vector
+    graphdata->size = -1;
+    KapaPrepPlot (kapa, xVec->n, graphdata);
+    KapaPlotVector (kapa, xVec->n, xVec->data.F32, "x");
+    KapaPlotVector (kapa, yVec->n, yVec->data.F32, "y");
+    KapaPlotVector (kapa, zVec->n, zScale->data.F32, "z");
+    psFree (zScale);
+    return true;
+}
+
+# else
+
+bool psastroPlotRawstars (psArray *rawstars, pmFPA *fpa, pmChip *chip, psMetadata *recipe)
+{
+    return false;
+}
+
+bool psastroPlotRefstars (psArray *refstars, psMetadata *recipe)
+{
+    return false;
+}
+
+bool psastroPlotOneChipFit (psArray *rawstars, psArray *refstars, psArray *match,
+    psMetadata *recipe)
+{
+    return false;
+}
+
+# endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.c.in
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.c.in	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.c.in	(revision 22306)
@@ -0,0 +1,25 @@
+/*
+ * The line
+    { PSASTRO_ERR_$X{ErrorCode}, "$X{ErrorDescription}"},
+ * (without the Xs)
+ * will be replaced by values from errorCodes.dat
+ */
+#include "pslib.h"
+#include "psastroErrorCodes.h"
+
+void psastroErrorRegister(void)
+{
+    static psErrorDescription errors[] = {
+       { PSASTRO_ERR_BASE, "First value we use; lower values belong to psLib" },
+       { PSASTRO_ERR_${ErrorCode}, "${ErrorDescription}"},
+    };
+    static int nerror = PSASTRO_ERR_NERROR - PSASTRO_ERR_BASE; // number of values in enum
+
+    for (int i = 0; i < nerror; i++) {
+       psErrorDescription *tmp = psAlloc(sizeof(psErrorDescription));
+       *tmp = errors[i];
+       psErrorRegister(tmp, 1);
+       psFree(tmp);			/* it's on the internal list */
+    }
+    nerror = 0;			                // don't register more than once
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.dat
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.dat	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.dat	(revision 22306)
@@ -0,0 +1,12 @@
+#
+# This file is used to generate pmErrorClasses.h
+#
+BASE = 300		First value we use; lower values belong to psLib
+UNKNOWN			Unknown PM error code
+NOT_IMPLEMENTED		Desired feature is not yet implemented
+ARGUMENTS		Incorrect arguments
+CONFIG			Problem in configure files
+IO			Problem in FITS I/O
+WCS      		Error interpreting FITS WCS information
+DATA                    Problem in data values
+REFSTARS                Problem in data values
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.h.in
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.h.in	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroErrorCodes.h.in	(revision 22306)
@@ -0,0 +1,18 @@
+#if !defined(PSASTRO_ERROR_CODES_H)
+#define PSASTRO_ERROR_CODES_H
+/*
+ * The line
+ *  PSASTRO_ERR_$X{ErrorCode},
+ * (without the X)
+ *
+ * will be replaced by values from errorCodes.dat
+ */
+typedef enum {
+    PSASTRO_ERR_BASE = 600,
+    PSASTRO_ERR_${ErrorCode},
+    PSASTRO_ERR_NERROR
+} psastroErrorCode;
+
+void psastroErrorRegister(void);
+
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChips.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChips.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChips.c	(revision 22306)
@@ -0,0 +1,258 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+// XXX I think the 'badAstrom' tests may need to be adjusted: see eg the nominal rotation of
+// the chips
+bool psastroFixChips (pmConfig *config, psMetadata *recipe) {
+
+  bool status;
+
+  bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+  if (!status) {
+      fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+  }
+  if (!fixChips) return true;
+
+  // identify reference astrometry table
+  // if not defined, correction was not requested; skip step
+  pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.MODEL");
+  if (!astrom) return true;
+
+  // select the input data sources
+  pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+  if (!input) {
+      psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+      return false;
+  }
+
+  // acceptable limits
+  double pixelTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.TOLERANCE");
+  if (!status) psAbort ("missing recipe value");
+
+  double angleTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.ANGLE.TOLERANCE");
+  if (!status) psAbort ("missing recipe value");
+
+  // make sure the astrometry model is loaded.  de-activate all files except PSASTRO.MODEL.
+  // supply the input concepts so the model is defined using the RA, DEC, POSANGLE of the input
+  // image.
+  psFree (astrom->fpa->concepts);
+  astrom->fpa->concepts = psMemIncrRefCounter (input->fpa->concepts);
+  pmFPAfileActivate (config->files, false, NULL);
+  pmFPAfileActivate (config->files, true, "PSASTRO.MODEL");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  // files associated with the science image
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) {
+      psError (PS_ERR_IO, false, "Can't load the astrometry model file");
+      return false;
+  }
+
+  // we now have a set of chip solutions and a set of prediction measure the overall
+  // offset and rotation of the two systems by comparing the chip corners, projected onto
+  // the focal plane (all 4 to prevent solutions tied to a single corner)
+
+  psVector *xObs = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+  psVector *yObs = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+  psVector *xRef = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+  psVector *yRef = psVectorAllocEmpty (4*input->fpa->chips->n, PS_TYPE_F32);
+
+  int nPts = 0;
+
+  pmChip *obsChip = NULL;
+  while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+      if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+      // set the chip astrometry using the astrom file
+      pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+      psRegion *region = pmChipPixels (obsChip);
+      psPlane ptCP, ptFP;
+
+      ptCP.x = region->x0; ptCP.y = region->y0;
+      psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+      xObs->data.F32[nPts] = ptFP.x;
+      yObs->data.F32[nPts] = ptFP.y;
+      psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+      xRef->data.F32[nPts] = ptFP.x;
+      yRef->data.F32[nPts] = ptFP.y;
+      nPts ++;
+
+      ptCP.x = region->x0; ptCP.y = region->y1;
+      psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+      xObs->data.F32[nPts] = ptFP.x;
+      yObs->data.F32[nPts] = ptFP.y;
+      psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+      xRef->data.F32[nPts] = ptFP.x;
+      yRef->data.F32[nPts] = ptFP.y;
+      nPts ++;
+
+      ptCP.x = region->x1; ptCP.y = region->y1;
+      psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+      xObs->data.F32[nPts] = ptFP.x;
+      yObs->data.F32[nPts] = ptFP.y;
+      psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+      xRef->data.F32[nPts] = ptFP.x;
+      yRef->data.F32[nPts] = ptFP.y;
+      nPts ++;
+
+      ptCP.x = region->x1; ptCP.y = region->y0;
+      psPlaneTransformApply (&ptFP, obsChip->toFPA, &ptCP);
+      xObs->data.F32[nPts] = ptFP.x;
+      yObs->data.F32[nPts] = ptFP.y;
+      psPlaneTransformApply (&ptFP, refChip->toFPA, &ptCP);
+      xRef->data.F32[nPts] = ptFP.x;
+      yRef->data.F32[nPts] = ptFP.y;
+      nPts ++;
+
+      psFree (region);
+  }
+  xObs->n = yObs->n = xRef->n = yRef->n = nPts;
+	
+  psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+  
+  psVector *mask = psVectorAlloc (nPts, PS_TYPE_U8);
+  psVectorInit (mask, 0);
+
+  psStats *stats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+  stats->clipIter = 1;
+
+  for (int i = 0; i < 3; i++) {
+      psVectorClipFitPolynomial2D (map->x, stats, mask, 0xff, xObs, NULL, xRef, yRef);
+      psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, xObs->n);
+
+      psVectorClipFitPolynomial2D (map->y, stats, mask, 0xff, yObs, NULL, xRef, yRef);
+      psTrace ("psModules.astrom", 3, "x resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, yObs->n);
+  }
+
+  // loop over all chips, select the outliers, and replace the measured astrometry with the model
+  // the measured transformation above must be applied to make the comparison, and also then applied to the 
+  // model transformation
+
+  psFree (xObs);
+  psFree (yObs);
+  psFree (xRef);
+  psFree (yRef);
+  psFree (mask);
+  psFree (stats);
+
+  psFree (view);
+  view = pmFPAviewAlloc (0);
+
+  while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+    if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+    // set the chip astrometry using the astrom file
+    pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+    // bad Astrometry test:  ref pixel or angle outside nominal
+
+    psPlane refPixel = {0.0, 0.0, 0.0, 0.0};
+    psPlane obsCoord, refCoord, tmpCoord;
+
+    // find location of 0,0 pixel in focal plane coords for this chip
+    psPlaneTransformApply (&obsCoord, obsChip->toFPA, &refPixel);
+
+    // find location of 0,0 pixel in focal plane coords for ref chip
+    // apply the global field rotation and offset before comparing
+    psPlaneTransformApply (&tmpCoord, refChip->toFPA, &refPixel);
+    psPlaneTransformApply (&refCoord, map, &tmpCoord);
+    
+    psPlane offPixel = {100.0, 0.0, 100.0, 0.0};
+    psPlane obsOffPt, refOffPt;
+
+    // find location of 0,0 pixel in focal plane coords for this chip
+    psPlaneTransformApply (&obsOffPt, obsChip->toFPA, &offPixel);
+
+    // find location of 0,0 pixel in focal plane coords for ref chip
+    psPlaneTransformApply (&tmpCoord, refChip->toFPA, &offPixel);
+    psPlaneTransformApply (&refOffPt, map, &tmpCoord);
+    
+    double obsAngle = PM_DEG_RAD*atan2 (obsOffPt.y - obsCoord.y, obsOffPt.x - obsCoord.x);
+    double refAngle = PM_DEG_RAD*atan2 (refOffPt.y - refCoord.y, refOffPt.x - refCoord.x);
+
+    bool badAstrom = false;
+    badAstrom |= fabs(obsCoord.x - refCoord.x) > pixelTol;
+    badAstrom |= fabs(obsCoord.y - refCoord.y) > pixelTol;
+    badAstrom |= fabs(obsAngle   - refAngle)   > angleTol;
+
+    fprintf (stderr, "chip %d, angle: %f, pixel: %f,%f\n", view->chip, obsAngle - refAngle, obsCoord.x - refCoord.x, obsCoord.y - refCoord.y);
+
+    if (!badAstrom) continue;
+
+    psLogMsg ("psastro", PS_LOG_INFO, "fixing chip %d, angle: %f, pixel: %f,%f\n",
+	      view->chip, obsAngle - refAngle, obsCoord.x - refCoord.x, obsCoord.y - refCoord.y);
+
+    psFree (obsChip->toFPA);
+    psFree (obsChip->fromFPA);
+
+    // 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 *toFPA = psPlaneTransformAlloc(refChip->toFPA->x->nX, refChip->toFPA->x->nY);
+    for (int i = 0; i <= refChip->toFPA->x->nX; i++) {
+	for (int j = 0; j <= refChip->toFPA->x->nY; j++) {
+	    double f1 = refChip->toFPA->x->coeffMask[i][j] ? 0.0 : map->x->coeff[1][0]*refChip->toFPA->x->coeff[i][j];
+	    double f2 = refChip->toFPA->y->coeffMask[i][j] ? 0.0 : map->x->coeff[0][1]*refChip->toFPA->y->coeff[i][j];
+	    toFPA->x->coeff[i][j] = f1 + f2;
+
+	    double g1 = refChip->toFPA->x->coeffMask[i][j] ? 0.0 : map->y->coeff[1][0]*refChip->toFPA->x->coeff[i][j];
+	    double g2 = refChip->toFPA->y->coeffMask[i][j] ? 0.0 : map->y->coeff[0][1]*refChip->toFPA->y->coeff[i][j];
+	    toFPA->y->coeff[i][j] = g1 + g2;
+	}
+    }
+    toFPA->x->coeff[0][0] += map->x->coeff[0][0];
+    toFPA->y->coeff[0][0] += map->y->coeff[0][0];
+
+    psRegion *region = pmChipPixels (obsChip);
+    obsChip->toFPA   = toFPA;
+    obsChip->fromFPA = psPlaneTransformInvert(NULL, obsChip->toFPA, *region, 50);
+    psFree (region);
+    
+    // XXX this stuff below should be applied to each readout...
+    // XXX for now, just use first readout
+    pmCell *cell = obsChip->cells->data[0];
+    pmReadout *readout = cell->readouts->data[0];
+
+    // use the new position to re-try the match fit
+    // select the raw objects for this readout
+    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+    if (rawstars == NULL) { continue; }
+
+    // select the raw objects for this readout
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+    if (refstars == NULL) { continue; }
+
+    // the absolute minimum number of stars is 4 (for order = 1)
+    if ((rawstars->n < 4) || (refstars->n < 4)) {
+	readout->data_exists = false;
+	psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)", 
+		  rawstars->n, refstars->n);
+	continue;
+    } 
+
+    psastroUpdateChipToFPA (input->fpa, obsChip, rawstars, refstars);
+
+    // update the header
+    psMetadata *updates = psMemIncrRefCounter (psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER"));
+    if (!updates) {
+	updates = psMetadataAlloc ();
+    }
+
+    // XXX update the header with info to reflect the failure
+    if (!psastroOneChipFit (input->fpa, obsChip, refstars, rawstars, recipe, updates)) {
+	readout->data_exists = false;
+	psLogMsg ("psastro", 3, "failed to find a solution\n");
+	psFree (updates);
+	continue;
+    }
+
+    pmAstromWriteWCS (updates, input->fpa, obsChip, NONLIN_TOL);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE | PS_DATA_METADATA, "psastro header stats", updates);
+    psFree (updates);
+  }
+
+  psFree (map);
+  psFree (view);
+  return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChipsTest.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChipsTest.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroFixChipsTest.c	(revision 22306)
@@ -0,0 +1,247 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psPlaneTransformPrint (FILE *f, psPlaneTransform *in) {
+
+  fprintf (f, "x (%d,%d):\n", in->x->nX, in->x->nY);
+  for (int i = 0; i <= in->x->nX; i++) {
+    fprintf (f, " %d : ", i);
+    for (int j = 0; j <= in->x->nY; j++) {
+      fprintf (f, "%f ", in->x->coeff[i][j]);
+    }
+    fprintf (f, "\n");
+  }
+  fprintf (f, "y (%d,%d):\n", in->y->nX, in->y->nY);
+  for (int i = 0; i <= in->y->nX; i++) {
+    fprintf (f, " %d : ", i);
+    for (int j = 0; j <= in->y->nY; j++) {
+      fprintf (f, "%f ", in->y->coeff[i][j]);
+    }
+    fprintf (f, "\n");
+  }
+  return TRUE;
+}
+
+// XXX the designed version is not working correctly. Here we load a model which just has
+// the CRPIX1, CRPIX2 values from another image. Compare those positions to the current
+// postions, make an average (DC) correction, then fix the outliers.
+bool psastroFixChipsTest (pmConfig *config, psMetadata *recipe) {
+
+    bool status;
+
+    bool fixChips = psMetadataLookupBool (&status, config->arguments, "PSASTRO.FIX.CHIPS");
+    if (!status) {
+	fixChips = psMetadataLookupBool (&status, recipe, "PSASTRO.FIX.CHIPS");
+    }
+    if (!fixChips) return true;
+
+    // identify reference astrometry table
+    // if not defined, correction was not requested; skip step
+    char *modelFile = psMetadataLookupStr (&status, recipe, "PSASTRO.ROUGH.MODEL");
+    if (!modelFile) return true;
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // acceptable limits
+    double pixelTol = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.TOLERANCE");
+    if (!status) psAbort ("missing recipe value");
+
+    FILE *f = fopen (modelFile, "r");
+    if (f == NULL) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find model data %s", modelFile);
+	return false;
+    }
+
+    float Xo, Yo;
+
+    psArray *refPos = psArrayAllocEmpty (100);
+    while (fscanf (f, "%*s %*d %*d %*f %f %f", &Xo, &Yo) != EOF) {
+	psPlane *pos = psPlaneAlloc();
+	pos->x = Xo;
+	pos->y = Yo;
+	psArrayAdd (refPos, 100, pos);
+	psFree (pos);
+    }
+    assert (refPos->n == 60);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // loop over all chips, get the current CRPIX1,CRPIX2 values
+    pmChip *obsChip = NULL;
+    psArray *obsPos = psArrayAllocEmpty (100);
+    while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+	psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+	if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+	// extract CRPIX1, CRPIX2:
+	// XXX for now, pull from the header:
+	// pmCell *cell = obsChip->cells->data[0];
+	// pmReadout *readout = cell->readouts->data[0];
+	// psMetadata *header = psMetadataLookupMetadata (&status, readout->analysis, "PSASTRO.HEADER");
+	// Xo = psMetadataLookupF32 (&status, header, "CRPIX1");
+	// Yo = psMetadataLookupF32 (&status, header, "CRPIX2");
+
+	pmAstromWCS *wcs = pmAstromWCSfromFPA(input->fpa, obsChip, NONLIN_TOL);
+	Xo = wcs->crpix1;
+	Yo = wcs->crpix2;
+	psFree (wcs);
+
+	psPlane *pos = psPlaneAlloc();
+	pos->x = Xo;
+	pos->y = Yo;
+	psArrayAdd (obsPos, 100, pos);
+	psFree (pos);
+    }
+    psFree (view);
+    assert (obsPos->n == 60);
+
+    // get the offsets
+    psVector *dX = psVectorAlloc (obsPos->n, PS_TYPE_F32);
+    psVector *dY = psVectorAlloc (obsPos->n, PS_TYPE_F32);
+    for (int i = 0; i < obsPos->n; i++) {
+	psPlane *obs = obsPos->data[i];
+	psPlane *ref = refPos->data[i];
+    
+	if (i < 30) {
+	    dX->data.F32[i] = obs->x - ref->x;
+	    dY->data.F32[i] = obs->y - ref->y;
+	} else {
+	    dX->data.F32[i] = ref->x - obs->x;
+	    dY->data.F32[i] = ref->y - obs->y;
+	}
+	fprintf (stderr, "chip %d : %f - %f : %f, %f - %f: %f\n", i, obs->x, ref->x, dX->data.F32[i], obs->y, ref->y, dY->data.F32[i]);
+    }
+    
+    // get the average offset
+    psStats *stats;
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psVectorStats (stats, dX, NULL, NULL, 0);
+    Xo = stats->robustMedian;
+    fprintf (stderr, "offset x: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psVectorStats (stats, dY, NULL, NULL, 0);
+    Yo = stats->robustMedian;
+    fprintf (stderr, "offset y: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    // measure the rotation of the good chips
+    psVector *dT = psVectorAllocEmpty (obsPos->n, PS_TYPE_F32);
+    for (int i = 0; i < dX->n; i++) {
+	bool badAstrom = false;
+	badAstrom |= (fabs(dX->data.F32[i] - Xo) > pixelTol);
+	badAstrom |= (fabs(dY->data.F32[i] - Yo) > pixelTol);
+	if (badAstrom) continue;
+
+	pmChip *chip = input->fpa->chips->data[i];
+	float theta = atan2 (chip->toFPA->x->coeff[0][1], chip->toFPA->x->coeff[1][0]);
+	if (i >= 30) {
+	  theta -= M_PI;
+	}
+	dT->data.F32[dT->n] = theta;
+	dT->n ++;
+    }
+    stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    psVectorStats (stats, dT, NULL, NULL, 0);
+    float To = stats->robustMedian;
+    fprintf (stderr, "offset t: %f +/- %f\n", stats->robustMedian, stats->robustStdev);
+    psFree (stats);
+
+    // find the discrepant chips and reset
+    for (int i = 0; i < dX->n; i++) {
+	bool badAstrom = false;
+	badAstrom |= (fabs(dX->data.F32[i] - Xo) > pixelTol);
+	badAstrom |= (fabs(dY->data.F32[i] - Yo) > pixelTol);
+	if (!badAstrom) continue;
+
+	fprintf (stderr, "fixing chip %d, offset: %f %f\n",
+		 i, dX->data.F32[i] - Xo, dY->data.F32[i] - Yo);
+
+	// XXX this stuff below should be applied to each readout...
+	// XXX for now, just use first readout
+	pmFPA  *fpa  = input->fpa;
+	pmChip *chip = fpa->chips->data[i];
+	pmCell *cell = chip->cells->data[0];
+	pmReadout *readout = cell->readouts->data[0];
+
+	pmAstromWCS *wcs = pmAstromWCSfromFPA(fpa, chip, NONLIN_TOL);
+
+	psPlane *ref = refPos->data[i];
+	wcs->crpix1 = ref->x + Xo;
+	wcs->crpix2 = ref->y + Yo;
+
+	pmAstromWCStoFPA (fpa, chip, wcs, pixelScale);
+	psFree (wcs);
+
+	// apply average rotation
+	psPlaneTransform *toFPA = psPlaneTransformRotate (NULL, chip->toFPA, To);
+	psFree (chip->toFPA);
+	chip->toFPA = toFPA;
+
+	psPlaneTransform *fromFPA = psPlaneTransformRotate (NULL, chip->fromFPA, -To);
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	// XXX did we get the offset right?
+	wcs = pmAstromWCSfromFPA(fpa, chip, NONLIN_TOL);
+	psFree (wcs);
+
+	// save WCS and analysis metadata in update header
+	psMetadata *updates = psMetadataAlloc();
+
+	// select the raw objects for this readout
+	psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+	if (rawstars == NULL) { continue; }
+
+	// select the raw objects for this readout
+	psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+	if (refstars == NULL) { continue; }
+
+	// the absolute minimum number of stars is 4 (for order = 1)
+	if ((rawstars->n < 4) || (refstars->n < 4)) {
+	    readout->data_exists = false;
+	    psLogMsg ("psastro", 3, "insufficient rawstars (%ld) or refstars (%ld)", 
+		      rawstars->n, refstars->n);
+	    continue;
+	} 
+
+	psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+
+	// XXX skip the re-fit step for a test
+	continue;
+
+	// XXX update the header with info to reflect the failure
+	if (!psastroOneChipFit (fpa, chip, refstars, rawstars, recipe, updates)) {
+	    readout->data_exists = false;
+	    psLogMsg ("psastro", 3, "failed to find a solution\n");
+	    psFree (updates);
+	    continue;
+	}
+
+	pmAstromWriteWCS (updates, fpa, chip, NONLIN_TOL);
+	psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_DATA_METADATA, "psastro header stats", updates);
+	psFree (updates);
+
+    }
+
+    psFree (dT);
+    psFree (dX);
+    psFree (dY);
+    psFree (refPos);
+    psFree (obsPos);
+  
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroInternal.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroInternal.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroInternal.h	(revision 22306)
@@ -0,0 +1,17 @@
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+# ifndef PSASTRO_INTERNAL_H
+# define PSASTRO_INTERNAL_H
+
+# include <stdio.h>
+# include <string.h>
+# include <strings.h>  // for strcasecmp
+# include <unistd.h>   // for unlink
+# include <pslib.h>
+# include <psmodules.h>
+# include <ppStats.h>
+# include "psastro.h"
+
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLoadRefstars.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLoadRefstars.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLoadRefstars.c	(revision 22306)
@@ -0,0 +1,223 @@
+# include "psastroInternal.h"
+# define ELIXIR_MODE 1
+
+// XXX other comment
+
+psArray *psastroLoadRefstars (pmConfig *config) {
+
+    int fd;
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+
+    // DVO APIs expect decimal degrees
+    float RAmin  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MIN");
+    float RAmax  = DEG_RAD*psMetadataLookupF32(NULL, recipe, "RA_MAX");
+    float DECmin = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MIN");
+    float DECmax = DEG_RAD*psMetadataLookupF32(NULL, recipe, "DEC_MAX");
+
+    // extra field fraction to add
+    double fieldPadding = psMetadataLookupF32 (&status, recipe, "PSASTRO.FIELD.PADDING");
+    PS_ASSERT (status, NULL);
+
+    float dRA = RAmax - RAmin;
+    RAmin -= dRA * fieldPadding;
+    RAmax += dRA * fieldPadding;
+
+    float dDEC = DECmax - DECmin;
+    DECmin -= dDEC * fieldPadding;
+    DECmax += dDEC * fieldPadding;
+
+    // grab the PSASTRO.CATDIR name from the PSASTRO recipe
+    char *catdir_recipe = psMetadataLookupStr(&status, recipe, "PSASTRO.CATDIR");
+    psAssert (catdir_recipe, "Need a recipe for the catdir!");
+
+    // substitute abstract name with concrete name, if present in PSASTRO.CATDIRS
+    psMetadata *catdirs = psMetadataLookupMetadata(&status, config->site, "PSASTRO.CATDIRS"); // List of cameras
+    if (!catdirs) {
+        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find PSASTRO.CATDIRS in the system configuration.\n");
+        return false;
+    }
+    char *catdir_virtual = psMetadataLookupStr(&status, catdirs, catdir_recipe);
+    char *catdir = (catdir_virtual == NULL) ? catdir_recipe : catdir_virtual;
+
+    // convert the uri to a real filename (ie, path://foobar)
+    psString CATDIR = pmConfigConvertFilename(catdir, config, false, false); // Resolved filename
+    PS_ASSERT (CATDIR, NULL);
+
+    char *getstarCommand = psStringCopy(psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR"));
+    PS_ASSERT (getstarCommand, NULL);
+
+    char *outformat = psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR.OUTFORMAT");
+    PS_ASSERT (outformat, NULL);
+
+    char *photcode = psMetadataLookupStr(NULL, recipe, "DVO.GETSTAR.PHOTCODE");
+    PS_ASSERT (photcode, NULL);
+
+    float MAGmax = psMetadataLookupF32(NULL, recipe, "DVO.GETSTAR.MAG.MAX");
+
+    // issue the following command:
+    // getstar -region RAmin RAmax DECmin DECmax
+    char tempFile[64];
+    sprintf (tempFile, "/tmp/psastro.XXXXXX");
+    if ((fd = mkstemp (tempFile)) == -1) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error creating temp output file\n");
+        psFree(CATDIR);
+        return NULL;
+    }
+    close (fd);
+
+    psTimerStart ("psastro");
+
+    // supply a known output format (for CATALOG output) so the code below knows what to read
+    if (ELIXIR_MODE) {
+        psStringAppend (&getstarCommand, " -D CATMODE mef -D CATFORMAT elixir");
+    } else {
+        psStringAppend (&getstarCommand, " -D CATMODE mef -D CATFORMAT panstarrs");
+    }
+
+    // check for default name (use .ptolemyrc), or use specified CATDIR
+    if (strcasecmp(CATDIR, "NONE")) {
+        psStringAppend (&getstarCommand, " -D CATDIR %s", CATDIR);
+    }
+    psFree(CATDIR);
+
+    // supply the max magnitude, the output format, and the photcode
+    if (strcasecmp (photcode, "NONE")) {
+        psStringAppend (&getstarCommand, " -maglim %f", MAGmax);
+    }
+    if (strcasecmp (photcode, "NONE")) {
+        psStringAppend (&getstarCommand, " -photcode %s", photcode);
+    }
+    psStringAppend (&getstarCommand, " -format %s", outformat);
+
+    // add region and output filename
+    psStringAppend (&getstarCommand, " -region %f %f %f %f -o %s", RAmin, DECmin, RAmax, DECmax, tempFile);
+    psTrace ("psastro", 3, "%s\n", getstarCommand);
+
+    // XXX use psPipe: catch stderr, stdout, allow for Nsec timeout...
+    // use fork to add timeout capability
+    status = system (getstarCommand);
+    if (status) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error loading reference data\n");
+        return NULL;
+    }
+    psFree (getstarCommand);
+
+    psLogMsg ("psastro", 3, "ran getstar : %f sec\n", psTimerMark ("psastro"));
+
+    // the output from getstar is a file with the Average table
+    psFits *fits = psFitsOpen (tempFile, "r");
+
+    psTimerStart ("psastro");
+
+    psArray *refstars = NULL;
+    if (!strcmp (outformat, "CATALOG")) {
+      refstars = psastroReadGetstarCatalog (fits);
+    }
+    if (!strcmp (outformat, "PS1_DEV_0")) {
+      refstars = psastroReadGetstar_PS1_DEV_0 (fits);
+    }
+    if (refstars == NULL) {
+        psError(PSASTRO_ERR_REFSTARS, true, "error reading reference data\n");
+        psFitsClose (fits);
+        return NULL;
+    }
+
+    psLogMsg ("psastro", 3, "loaded %ld reference stars : %f sec\n", refstars->n, psTimerMark ("psastro"));
+
+    psTrace ("psastro", 3, "loaded %ld reference stars from (%10.6f,%10.6f) - (%10.6f,%10.6f)\n",
+             refstars->n, RAmin, DECmin, RAmax, DECmax);
+
+    psFitsClose (fits);
+    unlink (tempFile);
+
+    // dump or plot the available refstars
+    if (psTraceGetLevel("psastro.dump") > 0) {
+        psastroDumpRefstars (refstars, "refstars.dat");
+    }
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+        psastroPlotRefstars (refstars, recipe);
+    }
+
+    return refstars;
+}
+
+psArray *psastroReadGetstarCatalog (psFits *fits) {
+
+    bool status;
+
+    if (ELIXIR_MODE) {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_ELIXIR");
+    } else {
+        psFitsMoveExtName (fits, "DVO_AVERAGE_PANSTARRS");
+    }
+
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+
+    // convert the Average table to the pmAstromObj entries
+    psArray *refstars = psArrayAllocEmpty (numSources);
+    for (int i = 0; i < numSources; i++) {
+        pmAstromObj *ref = pmAstromObjAlloc ();
+
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        // DVO tables are stored in degrees
+        if (ELIXIR_MODE) {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF32 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF32 (&status, row, "DEC");
+            ref->Mag      = 0.001*psMetadataLookupS32 (&status, row, "MAG");  // ELIXIR uses millimags
+        } else {
+            ref->sky->r   = RAD_DEG*psMetadataLookupF64 (&status, row, "RA");
+            ref->sky->d   = RAD_DEG*psMetadataLookupF64 (&status, row, "DEC");
+            ref->Mag      = psMetadataLookupF32 (&status, row, "MAG"); // PANSTARRS uses mags
+        }
+
+        // XXX VERY temporary hack to avoid M31 bulge
+        if ((fabs(ref->sky->r - 0.186438) < 0.002) && (fabs(ref->sky->d - 0.720270) < 0.002)) {
+          psFree (ref);
+          psFree (row);
+          continue;
+        }
+
+        psArrayAdd (refstars, 100, ref);
+        psFree (ref);
+        psFree (row);
+    }
+    return refstars;
+}
+
+psArray *psastroReadGetstar_PS1_DEV_0 (psFits *fits) {
+
+    bool status;
+
+    psFitsMoveExtName (fits, "GETSTAR_PS1_DEV_0");
+
+    long numSources = psFitsTableSize(fits); // Number of sources in table
+
+    // convert the Average table to the pmAstromObj entries
+    psArray *refstars = psArrayAllocEmpty (numSources);
+    for (int i = 0; i < numSources; i++) {
+        pmAstromObj *ref = pmAstromObjAlloc ();
+
+        psMetadata *row = psFitsReadTableRow(fits, i); // Table row
+
+        ref->sky->r   = RAD_DEG*psMetadataLookupF32 (&status, row, "RA");
+        ref->sky->d   = RAD_DEG*psMetadataLookupF32 (&status, row, "DEC");
+        ref->Mag      = psMetadataLookupF32 (&status, row, "MAG");
+
+        // XXX VERY temporary hack to avoid M31 bulge
+        if ((fabs(ref->sky->r - 0.186438) < 0.002) && (fabs(ref->sky->d - 0.720270) < 0.002)) {
+          psFree (ref);
+          psFree (row);
+          continue;
+        }
+
+        psArrayAdd (refstars, 100, ref);
+        psFree (ref);
+        psFree (row);
+    }
+    return refstars;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLuminosityFunction.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLuminosityFunction.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroLuminosityFunction.c	(revision 22306)
@@ -0,0 +1,141 @@
+# include "psastroInternal.h"
+# define dMag 0.1
+// XXX put this in config?
+
+static void pmLumFuncFree (pmLumFunc *func) {
+
+    if (func == NULL) return;
+
+    return;
+}
+
+pmLumFunc *pmLumFuncAlloc () {
+
+    pmLumFunc *func = (pmLumFunc *) psAlloc(sizeof(pmLumFunc));
+    psMemSetDeallocator(func, (psFreeFunc) pmLumFuncFree);
+
+    func->mMin = 0.0;
+    func->mMax = 0.0;
+    func->slope = 0.0;
+    func->offset = 0.0;
+
+    func->mPeak = 0.0;
+    func->nPeak = 0;
+    func->sPeak = 0;
+
+    return func;
+}
+
+pmLumFunc *psastroLuminosityFunction (psArray *stars) {
+
+    if (stars->n == 0) return NULL;
+
+    // determine the max and min magnitude for the array of stars
+    pmAstromObj *star = stars->data[0];
+    double mMin = star->Mag;
+    double mMax = star->Mag;
+    for (int i = 0; i < stars->n; i++) {
+	star = stars->data[i];
+	mMin = PS_MIN (star->Mag, mMin);
+	mMax = PS_MAX (star->Mag, mMax);
+    }
+
+    int nBin = 1 + (mMax - mMin) / dMag;
+    if (nBin <= 1) {
+	psLogMsg ("psastro", 4, "insufficient valid stars for this readout\n");
+	return NULL;
+    }
+
+    // create a histogram of the magnitudes
+    // bin[0] = mMin : mMin + dMag
+    // bin[i] = mMin + i*dMag : mMin + (i+1)*dMag
+    psVector *nMags = psVectorAlloc (nBin, PS_TYPE_F32);
+    psVectorInit (nMags, 0);
+
+    for (int i = 0; i < stars->n; i++) {
+	star = stars->data[i];
+	if (!isfinite(star->Mag)) continue;
+	int bin = (star->Mag - mMin) / dMag;
+	if (bin < 0) psAbort("bin cannot be negative!");
+	if (bin >= nBin) psAbort("bin cannot be > %d!", nBin);
+	nMags->data.F32[bin] += 1.0;
+    }
+
+    // find the peak and position
+    int iPeak = 0;
+    int nPeak = 0;
+    int sPeak = 0;
+    for (int i = 0; i < nMags->n; i++) {
+	if (nMags->data.F32[i] < nPeak) continue;
+	iPeak = i;
+	nPeak = nMags->data.F32[i];
+	sPeak += nPeak;
+    }
+
+    psVector *lnMag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+    psVector *Mag = psVectorAllocEmpty (nBin, PS_TYPE_F32);
+
+    // create 2 vectors represnting the dlogN/dlogS line
+    // exclude bins with no stars
+    // exclude points after the peak with N < 0.8*peak
+    int n = 0;
+    for (int i = 0; i < nMags->n; i++) {
+	if (nMags->data.F32[i] < 1) continue;
+	if ((i > iPeak) && (nMags->data.F32[i] < 0.8*nPeak)) continue;
+	lnMag->data.F32[n] = log10 (nMags->data.F32[i]);
+	Mag->data.F32[n] = mMin + (i + 0.5)*dMag;
+	n++;
+    }
+    lnMag->n = n;
+    Mag->n = n;
+    psLogMsg ("psastro", 4, "fitting %d points to luminosity function\n", n);
+
+    psVector *mask = psVectorAlloc (Mag->n, PS_TYPE_MASK);
+    psVectorInit (mask, 0);
+
+    psPolynomial1D *line = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipSigma = 3.0;
+    stats->clipIter = 3;
+
+    if (!psVectorClipFitPolynomial1D(line, stats, mask, 0xff, lnMag, NULL, Mag)) {
+	psLogMsg ("psastro", 4, "Failed to fit the luminosity function\n");
+	return(NULL);
+    }
+
+    // find min and max unmasked magnitudes
+    double mMinValid = NAN;
+    double mMaxValid = NAN;
+    for (int i = 0; i < Mag->n; i++) {
+	if (mask->data.U8[i]) continue;
+	if (isnan(mMinValid) || (Mag->data.F32[i] < mMinValid)) {
+	    mMinValid = Mag->data.F32[i];
+	}
+	if (isnan(mMaxValid) || (Mag->data.F32[i] > mMaxValid)) {
+	    mMaxValid = Mag->data.F32[i];
+	}
+    }
+
+    psLogMsg ("psastro", 4, "logN = %f + %f mag\n", line->coeff[0], line->coeff[1]);
+    psLogMsg ("psastro", 4, "logN vs mag residuals: %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+
+    pmLumFunc *lumFunc = pmLumFuncAlloc ();
+    lumFunc->mMin = mMinValid;
+    lumFunc->mMax = mMaxValid;
+    lumFunc->offset = line->coeff[0];
+    lumFunc->slope = line->coeff[1];
+    lumFunc->mPeak = mMin + (iPeak + 0.5)*dMag;
+    lumFunc->nPeak = nPeak;
+    lumFunc->sPeak = sPeak;
+
+    psFree (lnMag);
+    psFree (nMags);
+    psFree (Mag);
+    psFree (mask);
+    psFree (line);
+    psFree (stats);
+
+    return (lumFunc);
+}
+
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMetadataStats.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMetadataStats.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMetadataStats.c	(revision 22306)
@@ -0,0 +1,68 @@
+# include "psastroInternal.h"
+
+bool psastroMetadataStats (pmConfig *config) {
+
+    bool status;
+
+    char *filename = psMetadataLookupStr (&status, config->arguments, "STATS");
+    if (!filename) return true; // stats not requested
+
+    // Extract statistics from the last output fpa
+    pmFPAfile *output = psMetadataLookupPtr(&status, config->files, "PSASTRO.OUTPUT");
+
+    if (!output) {
+	psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find output file (PSASTRO.OUTPUT).");
+	return false;
+    }
+
+    // create output stats metadata
+    psMetadata *stats = psMetadataAlloc ();
+
+    // extract stats for the complete fpa 
+    pmFPAview *view = pmFPAviewAlloc(0);
+
+    if (!ppStatsMetadata(stats, output->fpa, view, 0, config)) {
+	psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate stats for image.");
+	psFree(view);
+	psFree(stats);
+	return false;
+    }
+
+    // if we did not request any specific stats, the structure is empty
+    if (stats && stats->list->n == 0) {
+	psWarning ("stats output specified, but no requested stats entries in headers");
+	psFree(view);
+	psFree(stats);
+	return true;
+    }
+
+    // convert the stats MDC to a string
+    char *statsMDC = psMetadataConfigFormat(stats);
+    if (!statsMDC || strlen(statsMDC) == 0) {
+	psError(PS_ERR_IO, false, "Unable to serialize stats metadata.\n");
+	return false;
+    } 
+
+    // convert to a real UNIX filename
+    psString resolved = pmConfigConvertFilename(filename, config, true, false); // Resolved filename
+    FILE *statsFile = fopen (resolved, "w");
+    if (!statsFile) {
+	psError(PS_ERR_IO, true, "Unable to open statistics file %s for writing.\n", resolved);
+	psFree(stats);
+	psFree(view);
+	psFree(statsMDC);
+	psFree(resolved);
+	return false;
+    }
+
+    // write the stats MDC to a file
+    // XXX why does this not call psMetadataConfigPrint?
+    fprintf(statsFile, "%s", statsMDC);
+    fclose(statsFile);
+
+    psFree(resolved);
+    psFree(statsMDC);
+    psFree(view);
+    psFree(stats);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModel.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModel.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModel.c	(revision 22306)
@@ -0,0 +1,53 @@
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    pmConfig *config = NULL;
+
+    psTimerStart ("complete");
+
+    psastroErrorRegister();              // register our error codes/messages
+
+    // model inits are needed in pmSourceIO
+    // models defined in psphot/src/models are not available in psastro
+    pmModelClassInit ();
+
+    // load configuration information
+    config = psastroModelArguments (argc, argv);
+
+    // load identify the data sources
+    if (!psastroModelParseCamera (config)) {
+	psErrorStackPrint(stderr, "error setting up the camera\n");
+	exit (1);
+    }
+
+    // load the raw pixel data (from PSPHOT.SOURCES)
+    // select subset of stars for astrometry
+    if (!psastroModelDataLoad (config)) {
+	psErrorStackPrint(stderr, "error loading input data\n");
+	exit (1);
+    }
+
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroModelAnalysis (config)) {
+	psErrorStackPrint(stderr, "failure in psastro model analysis\n");
+	exit (1);
+    }
+    
+    // run the full astrometry analysis (chip and/or mosaic)
+    if (!psastroModelAdjust (config)) {
+	psErrorStackPrint(stderr, "failure in psastro model adjust\n");
+	exit (1);
+    }
+    
+    // save the model
+    if (!psastroModelDataSave (config)) {
+	psErrorStackPrint(stderr, "error saving output data\n");
+	exit (1);
+    }
+
+    psLogMsg ("psastro", 3, "complete psastro run: %f sec\n", psTimerMark ("complete"));
+
+    // psastroCleanup (config);
+    exit (EXIT_SUCCESS);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAdjust.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAdjust.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAdjust.c	(revision 22306)
@@ -0,0 +1,245 @@
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+# define DEBUG 0
+
+bool psastroModelAdjustBoresite (pmFPAfile *output, pmChip *refChip);
+
+bool psastroModelAdjust (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    // if we have not measured the boresite position, no adjustment is needed
+    bool fitBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.FIT.BORESITE");
+    if (!status) psAbort ("Can't find recipe option PSASTRO.MODEL.FIT.BORESITE");
+
+    // as an alternative to fit the boresite from a rotation sequence, we can set the boresite
+    // relative to the reference chip coordinates
+    bool setBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.SET.BORESITE");
+    if (!status) psAbort ("Can't find recipe option PSASTRO.MODEL.SET.BORESITE");
+
+    if (fitBoresite && setBoresite) {
+	psError(PS_ERR_IO, true, "invalid to choose both FIT.BORESITE and SET.BORESITE"); 
+	return false; 
+    } 
+	
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+    if (!output->fpa) psAbort ("no existing input fpa contains the reference chip");
+
+    // physical pixel scale in microns per pixel
+    char *refChipName = psMetadataLookupStr (&status, recipe, "PSASTRO.MODEL.REF.CHIP");
+    if (!refChipName) {
+	psError(PS_ERR_IO, true, "reference chip is missing from recipe"); 
+	return false; 
+    } 
+
+    // get reference chip from name
+    pmChip *refChip = pmConceptsChipFromName (output->fpa, refChipName);
+    if (!refChip) psAbort ("invalid chip name for reference");
+    if (!refChip->toFPA) psAbort ("invalid astrometry for reference chip");
+
+    // save the TPA region for tranformation inversions below
+    // psRegion *tpaRegion = pmAstromFPInTP (output->fpa);
+    psRegion *fpaRegion = pmAstromFPAExtent (output->fpa);
+
+    if (DEBUG) psastroDumpCorners ("corners.up.raw.dat", "corners.dn.raw.dat", output->fpa);
+
+    if (setBoresite) {
+	float boreXchip = psMetadataLookupF32 (&status, recipe, "PSASTRO.MODEL.BORESITE.X");
+	float boreYchip = psMetadataLookupF32 (&status, recipe, "PSASTRO.MODEL.BORESITE.Y");
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", boreXchip); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", boreYchip); 
+    }
+
+    if (fitBoresite || setBoresite) {
+	psastroModelAdjustBoresite (output, refChip);
+    } else {
+	// FPA.BORE.X0,Y0 should be 0,0 in the focal plane, not the chip.  Ask for the
+	// coordinates which make refChip->toFPA(x,y) = (0,0)
+	psPlane *PT = psPlaneTransformGetCenter (refChip->toFPA, NONLIN_TOL);
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", PT->x); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", PT->y); 
+	psFree (PT);
+    }
+
+    // rotate the chip-to-FPA transforms to have 0.0 posangle for refChip; 
+    // compensate by rotating fpa to TPA transform
+
+    // get the current posangle of the ref chip
+    float chipAngle = atan2 (refChip->toFPA->y->coeff[1][0], refChip->toFPA->x->coeff[1][0]);
+    fprintf (stderr, "chipAngle: %f\n", chipAngle*PS_DEG_RAD);
+    // psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE, "boresite parameter", posangle);
+
+    // rotate the chip transforms
+    for (int i = 0; i < output->fpa->chips->n; i++) {
+	pmChip *chip = output->fpa->chips->data[i];
+	if (!chip->toFPA) continue;
+	// skip chips without astrometry
+
+	// save the region of this chip for the inversion below
+	psRegion *region = pmChipPixels (chip);
+
+	psPlaneTransform *toFPA = psPlaneTransformRotate (NULL, chip->toFPA, chipAngle);
+	psFree (chip->toFPA);
+	chip->toFPA = toFPA;
+
+	// invert the new fromFPA transform to get the new toFPA transform
+	psPlaneTransform *fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50);
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	psFree (region);
+
+	// save the transformation in the header
+	pmAstromWriteBilevelChip (chip->hdu->header, chip, NONLIN_TOL);
+    }
+
+    // get the current posangle of the fpa
+    float fpaAngle = atan2 (output->fpa->toTPA->y->coeff[1][0], output->fpa->toTPA->x->coeff[1][0]);
+    fprintf (stderr, "fpaAngle: %f\n", fpaAngle*PS_DEG_RAD);
+    // psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POSANGLE", PS_META_REPLACE, "boresite parameter", posangle);
+
+    // remove the fpa rotation to generate a rotation-free model
+    psPlaneTransform *toTPA = psPlaneTransformRotate (NULL, output->fpa->toTPA, fpaAngle);
+    psFree (output->fpa->toTPA);
+    output->fpa->toTPA = toTPA;
+
+    psFree (output->fpa->fromTPA);
+    output->fpa->fromTPA = psPlaneTransformInvert(NULL, output->fpa->toTPA, *fpaRegion, 50);
+
+    // the model now describes the unrotated focal-plane
+    if (DEBUG) psastroDumpCorners ("corners.up.rot.dat", "corners.dn.rot.dat", output->fpa);
+
+    psMetadata *header = output->fpa->hdu->header;
+    pmAstromWriteBilevelMosaic (header, output->fpa, NONLIN_TOL);
+
+    psFree (fpaRegion);
+
+    return true;
+}
+
+bool psastroModelAdjustBoresite (pmFPAfile *output, pmChip *refChip) {
+
+    bool status;
+
+    psPlane  *boreCH  = psPlaneAlloc();
+    psPlane  *boreFP  = psPlaneAlloc();    
+    psPlane  *boreTP  = psPlaneAlloc();    
+    psSphere *boreSky = psSphereAlloc();    
+
+    // correct Xo,Yo to Lo,Mo using the ref chip toFPA
+    // ref chip position of the true boresite center
+    boreCH->x = psMetadataLookupF32 (&status, output->fpa->concepts, "FPA.BORE.X0"); 
+    boreCH->y = psMetadataLookupF32 (&status, output->fpa->concepts, "FPA.BORE.Y0"); 
+    psPlaneTransformApply (boreFP, refChip->toFPA, boreCH);
+
+    // adjust the reference pixel for all chips
+    for (int i = 0; i < output->fpa->chips->n; i++) {
+	pmChip *chip = output->fpa->chips->data[i];
+	if (!chip->fromFPA) continue;
+	// skip the chips without astrometry
+
+	// save the FPA region of this chip for the inversion below
+	psRegion *region = pmChipPixels (chip);
+
+	// the current toFPA returns boreFP->x,y for the boresite; subtract this from the transformations
+	chip->toFPA->x->coeff[0][0] -= boreFP->x;
+	chip->toFPA->y->coeff[0][0] -= boreFP->y;
+
+	// psPlaneTransform *toFPA = psPlaneTransformSetCenter (NULL, chip->toFPA, -boreFP->x, -boreFP->y);
+	// psFree (chip->toFPA);
+	// chip->toFPA = toFPA;
+
+	// invert the new fromFPA transform to get the new toFPA transform
+	// the region used here is the region covered by the chip in the FPA
+	psPlaneTransform *fromFPA = psPlaneTransformInvert(NULL, chip->toFPA, *region, 50); 
+	psFree (chip->fromFPA);
+	chip->fromFPA = fromFPA;
+
+	psFree (region);
+    }
+
+    if (DEBUG) psastroDumpCorners ("corners.up.shf.dat", "corners.dn.shf.dat", output->fpa);
+
+    // we have now adjusted the chips to use the correct boresite position as the center of the focal-plane system.
+    // we now need to reconstruct the TP to FP transformation, starting from stars projected about this new boresite position.
+
+    // find the R,D of the new boresite (boreFP -> 0,0; 0,0 -> -boreFP)
+    boreFP->x = -boreFP->x;
+    boreFP->y = -boreFP->y;
+    psPlaneTransformApply (boreTP, output->fpa->toTPA, boreFP);
+    psDeproject (boreSky, boreTP, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    psProjection *newSky = psProjectionAlloc (boreSky->r, boreSky->d, output->fpa->toSky->Xs, output->fpa->toSky->Ys, output->fpa->toSky->type);
+
+    // generate a collection of points on the sky using the old toTPA transformation and toSky projection, projected with the newSky projection
+    // this is the FPA coordinate range covered by the FP: 
+    psRegion *fpaRegion = pmAstromFPAExtent (output->fpa);
+    float dx = (fpaRegion->x1 - fpaRegion->x0) / 50.0;
+    float dy = (fpaRegion->y1 - fpaRegion->y0) / 50.0;
+
+    psPlane fp, tp;
+    psSphere sky;
+
+    psVector *FPx = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *FPy = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *TPx = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *TPy = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    // XXX a test: boreFP->x,y, should transform to tp.x,y = 0,0
+    fp.x = boreFP->x;
+    fp.y = boreFP->y;
+    psPlaneTransformApply (&tp, output->fpa->toTPA, &fp);
+    psDeproject (&sky, &tp, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+    psProject (&tp, &sky, newSky); // find the RA,DEC coord of the focal-plane coordinate
+
+    int Npts = 0;
+    for (fp.x = fpaRegion->x0; fp.x <= fpaRegion->x1; fp.x += dx) {
+	for (fp.y = fpaRegion->y0; fp.y <= fpaRegion->y1; fp.y += dy) {
+	    psPlaneTransformApply (&tp, output->fpa->toTPA, &fp);
+	    psDeproject (&sky, &tp, output->fpa->toSky); // find the RA,DEC coord of the focal-plane coordinate
+	    psProject (&tp, &sky, newSky); // find the RA,DEC coord of the focal-plane coordinate
+
+	    // we are fitting points in the NEW FP system to points in the NEW TP system
+	    FPx->data.F32[Npts] = fp.x - boreFP->x;
+	    FPy->data.F32[Npts] = fp.y - boreFP->y;
+	    TPx->data.F32[Npts] = tp.x;
+	    TPy->data.F32[Npts] = tp.y;
+	    psVectorExtend (FPx, 100, 1);
+	    psVectorExtend (FPy, 100, 1);
+	    psVectorExtend (TPx, 100, 1);
+	    psVectorExtend (TPy, 100, 1);
+	    Npts ++;
+	}
+    }
+    psFree (fpaRegion);
+
+    // fit both up and down transformations to the same points
+    psVectorFitPolynomial2D (output->fpa->toTPA->x, NULL, 0, TPx, NULL, FPx, FPy);
+    psVectorFitPolynomial2D (output->fpa->toTPA->y, NULL, 0, TPy, NULL, FPx, FPy);
+    psVectorFitPolynomial2D (output->fpa->fromTPA->x, NULL, 0, FPx, NULL, TPx, TPy);
+    psVectorFitPolynomial2D (output->fpa->fromTPA->y, NULL, 0, FPy, NULL, TPx, TPy);
+
+    psFree (output->fpa->toSky);
+    output->fpa->toSky = newSky;
+
+    if (DEBUG) psastroDumpCorners ("corners.up.bore.dat", "corners.dn.bore.dat", output->fpa);
+
+    psFree (FPx);
+    psFree (FPy);
+    psFree (TPx);
+    psFree (TPy);
+
+    psFree (boreCH);
+    psFree (boreFP);
+    psFree (boreTP);
+    psFree (boreSky);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAnalysis.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAnalysis.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelAnalysis.c	(revision 22306)
@@ -0,0 +1,162 @@
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001
+
+bool psastroModelAnalysis (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    // reference chip for boresite parameters
+    char *refChip = psMetadataLookupStr (&status, recipe, "PSASTRO.MODEL.REF.CHIP");
+    if (!refChip) {
+	psError(PS_ERR_IO, true, "reference chip is missing from recipe"); 
+	return false; 
+    } 
+
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+
+    // measure the boresite position from a rotation sequence?
+    bool fitBoresite = psMetadataLookupBool (&status, recipe, "PSASTRO.MODEL.FIT.BORESITE");
+    if (!fitBoresite) {
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RX", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RY", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.T0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.P0", PS_META_REPLACE, "boresite parameter", 0.0); 
+	psMetadataAddStr (output->fpa->concepts, PS_LIST_TAIL, "FPA.REF.CHIP", PS_META_REPLACE, "boresite parameter", refChip);
+	return true; 
+    } 
+
+    char *outroot = psMetadataLookupStr (&status, config->arguments, "OUTPUT");
+    if (!status || !outroot) psAbort ("Can't find outroot on config->arguments");
+
+    /* model analysis:
+     *
+     * determine POS_ZERO via comparison of measured and reported posangles
+     * POS_ZERO = FPA.POSANGLE - posangle
+     *
+     * determine boresite model:
+     * X = Xo + R_X cos(FPA.POSANGLE - T_0) cos(P_0) + R_Y sin(FPA.POSANGLE - T_0) sin(P_0) 
+     * Y = Yo + R_Y sin(FPA.POSANGLE - T_0) cos(P_0) - R_X cos(FPA.POSANGLE - T_0) sin(P_0) 
+     * position of reported boresite in reference chip pixels
+     * Xo, Yo : true coordinate of boresite (rotator center) in reference chip pixels
+     * R_X, R_Y : amplitude of boresite offset
+     * T_0 : reference angle for rotator
+     * P_0 : orientation of boresite ellipse
+     *
+     */
+
+    // select the input pmFPAfile pointers
+    psMetadataItem *item = psMetadataLookup (config->files, "PSASTRO.WCS");
+    if (item == NULL) psAbort("missing PSASTRO.WCS entries in config->files");
+    if (item->type != PS_DATA_METADATA_MULTI) psAbort("unexpected type for PSASTRO.WCS");
+    psArray *files = psListToArray (item->data.list);
+
+    // data storage vectors for measurements
+    psVector *posZero  = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Po       = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Xo       = psVectorAlloc (files->n, PS_TYPE_F32);
+    psVector *Yo       = psVectorAlloc (files->n, PS_TYPE_F32);
+
+    // counter for accepted measured values
+    int n = 0;
+
+    // Re-select the output fpa.  The output fpa needs to have valid astrometry for the
+    // refChip.  output->fpa is a copy of the pointer to one of the input->fpa, but the choice
+    // is arbitrary.  select a new one that has an existing ref chip
+    psFree (output->fpa);
+    output->fpa = NULL;
+
+    char filename[256];
+    snprintf (filename, 256, "%s.bore", outroot);
+    FILE *outfile = fopen (filename, "w");
+    if (!outfile) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "cannot open %s for output", filename);
+	return false;
+    }
+
+    float posBoundary = 0.0;
+
+    // extract the relevant measured and reported values from the reference chip
+    for (int i = 0; i < files->n; i++) {
+	psMetadataItem *file = files->data[i];
+	pmFPAfile *input = file->data.V;
+
+	// reported rotator position angle
+	double POSANGLE = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.POSANGLE"); 
+	if (!status) psAbort ("missing FPA.POSANGLE");
+
+    	// get reference chip from name
+	pmChip *chip = pmConceptsChipFromName (input->fpa, refChip);
+	if (!chip) psAbort ("invalid chip name for reference");
+
+	if (!chip->toFPA) continue;
+
+	if (!output->fpa) {
+	    // this one matches
+	    output->fpa = psMemIncrRefCounter(input->fpa);
+	}
+
+	// we have two measurements of the posangle (may be parity flipped to different quadrants)
+	// atan2 returns values in the range 0-2pi
+	// all posZero values should be clustered in some region, but we need to flip over the 0,360 boundary correctly.
+	// push all to one side or the other
+	float chipAngle = PM_DEG_RAD * atan2 (chip->toFPA->y->coeff[1][0], chip->toFPA->x->coeff[1][0]);
+	float fpaAngle = PM_DEG_RAD * atan2 (input->fpa->toTPA->y->coeff[1][0], input->fpa->toTPA->x->coeff[1][0]);
+
+	posZero->data.F32[n] = POSANGLE - chipAngle - fpaAngle;
+	if (n == 0) {
+	    posBoundary = posZero->data.F32[n] + 180.0;
+	} else {
+	    while (posZero->data.F32[n] > posBoundary) posZero->data.F32[n] -= 360.0;
+	    while (posZero->data.F32[n] < posBoundary - 360.0) posZero->data.F32[n] += 360.0;
+	}
+
+	Po->data.F32[n] = POSANGLE * PM_RAD_DEG; // reported position angle
+	float xc = chip->fromFPA->x->coeff[0][0]; // reported boresite x position in ref chip coordinates
+	float yc = chip->fromFPA->y->coeff[0][0]; // reported boresite y position in ref chip coordinates
+	// XXX this can also be derived from toFPA via GetCenter....
+	
+	psPlane *PT = psPlaneTransformGetCenter (chip->toFPA, NONLIN_TOL);
+	Xo->data.F32[n] = PT->x; // reported boresite x position in ref chip coordinates
+	Yo->data.F32[n] = PT->y; // reported boresite y position in ref chip coordinates
+	psFree (PT);
+
+	fprintf (outfile, "%d : %f %f : %f = %f - %f - %f | %f %f\n", i, Xo->data.F32[n], Yo->data.F32[n], posZero->data.F32[n], POSANGLE, chipAngle, fpaAngle, xc, yc);
+	n ++;
+    }
+      
+    Xo->n = n;
+    Yo->n = n;
+    Po->n = n;
+    posZero->n = n;
+
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    psVectorStats (stats, posZero, NULL, NULL, 0);
+
+    fprintf (outfile, "# pos zero %f +/- %f\n", stats->sampleMedian, stats->sampleStdev);
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.POS_ZERO", PS_META_REPLACE, "offset between obs and meas posangle", stats->sampleMedian); 
+    fclose (outfile);
+
+    psVector *params = psastroModelFitBoresite (Xo, Yo, Po, outroot);
+    if (params->n != 6) psAbort ("error");
+
+
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.X0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_X0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.Y0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_Y0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RX", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_RX]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.RY", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_RY]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.T0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_T0]); 
+    psMetadataAddF32 (output->fpa->concepts, PS_LIST_TAIL, "FPA.BORE.P0", PS_META_REPLACE, "boresite parameter", params->data.F32[PAR_P0]); 
+    psMetadataAddStr (output->fpa->concepts, PS_LIST_TAIL, "FPA.REF.CHIP", PS_META_REPLACE, "boresite parameter", refChip);
+
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelArguments.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelArguments.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelArguments.c	(revision 22306)
@@ -0,0 +1,63 @@
+# include "psastroStandAlone.h"
+
+static char *usage = "USAGE: psastroModel [-output root] (cmffiles)";
+
+pmConfig *psastroModelArguments (int argc, char **argv) {
+
+    int N;
+
+    if (argc == 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "No arguments supplied");
+        psErrorStackPrint(stderr, usage);
+	exit (1);
+    }
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSASTRO_RECIPE);
+    if (config == NULL) {
+        psError(PSASTRO_ERR_CONFIG, false, "Can't read site configuration");
+        psErrorStackPrint(stderr, usage);
+	exit (1);
+    }
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSASTRO recipe values loaded from recipe files
+
+    // XXX no options yet defined
+    // psMetadata *options = pmConfigRecipeOptions (config, PSASTRO_RECIPE);
+
+    // define the output filename
+    if ((N = psArgumentGet (argc, argv, "-output"))) {
+        psArgumentRemove (N, &argc, argv);
+	psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    } else {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Missing -output (root)");
+        psErrorStackPrint(stderr, usage);
+	exit (1);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+    
+    if (argc < 1) {
+        psError(PSASTRO_ERR_ARGUMENTS, true, "Incorrect arguments supplied");
+        psErrorStackPrint(stderr, "exit");
+	exit (1);
+    }
+
+    // each additional word is a file; create names INPUT.%d for them
+    for (int i = 0; i < argc - 1; i++) {
+	char name[16];
+	snprintf (name, 16, "INPUT.%d", i);
+	psArray *array = psArrayAlloc(1);
+	array->data[0] = psStringCopy (argv[i+1]);
+	psMetadataAddPtr(config->arguments, PS_LIST_TAIL, name,  PS_DATA_ARRAY, "", array);
+    }	
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "INPUT.N", 0, "", argc - 1);
+    return (config);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelBoresite.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelBoresite.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelBoresite.c	(revision 22306)
@@ -0,0 +1,52 @@
+# include "psastroStandAlone.h"
+
+// the full chisq is built of two associated sums over coordinates:
+// chisq = sum ((X_obs - X_fit(t))^2 + (Y_obs - Y_fit(t))^2)
+// we use split this into a 2x long vector and use coord[1] to distinguish the X and Y terms:
+// coord[0] = measured X or measured Y
+// coord[1] =          0 or          1
+psF32 psastroModelBoresite (psVector *deriv, const psVector *params, const psVector *coord) {
+
+    psF32 *PAR = params->data.F32;
+
+    float csphi = cos(PAR[PAR_P0]);
+    float snphi = sin(PAR[PAR_P0]);
+
+    float dtheta = coord->data.F32[0] - PAR[PAR_T0];
+    float cstht = cos(dtheta);
+    float sntht = sin(dtheta);
+
+    // value is X
+    if (coord->data.F32[1] == 0) {
+
+	float value = PAR[PAR_X0] + PAR[PAR_RX]*cstht*csphi + PAR[PAR_RY]*sntht*snphi;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_X0] = 1.0;
+	    dPAR[PAR_Y0] = 0.0;
+	    dPAR[PAR_RX] = +cstht*csphi;
+	    dPAR[PAR_RY] = +sntht*snphi;
+	    dPAR[PAR_P0] = -PAR[PAR_RX]*cstht*snphi + PAR[PAR_RY]*sntht*csphi;
+	    dPAR[PAR_T0] =  PAR[PAR_RX]*sntht*csphi - PAR[PAR_RY]*cstht*snphi;
+	}
+	return (value);
+    }  
+
+    // value is Y
+    if (coord->data.F32[1] == 1) {
+	float value = PAR[PAR_Y0] + PAR[PAR_RY]*sntht*csphi - PAR[PAR_RX]*cstht*snphi;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_X0]  = 0.0;
+	    dPAR[PAR_Y0]  = 1.0;
+	    dPAR[PAR_RX]  = -cstht*snphi;
+	    dPAR[PAR_RY]  = +sntht*csphi;
+	    dPAR[PAR_P0]  = -PAR[PAR_RY]*sntht*snphi - PAR[PAR_RX]*cstht*csphi;
+	    dPAR[PAR_T0]  = -PAR[PAR_RY]*cstht*csphi - PAR[PAR_RX]*sntht*snphi;
+	}
+	return (value);
+    }  
+    psAbort ("programming error: invalid coordinate");
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataLoad.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataLoad.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataLoad.c	(revision 22306)
@@ -0,0 +1,100 @@
+# include "psastroStandAlone.h"
+
+// this loop loads the header data from the input files, using the output pmFPAfile to guide
+// the chip selection and related issues
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroModelDataLoad"); \
+  psFree (view); \
+  return false; \
+}
+  
+// all of the different astrometry analysis modes use the same data load loop
+bool psastroModelDataLoad (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+
+    psTimerStart ("psastro");
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // physical pixel scale in microns per pixel
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, true, "Failed to lookup pixel scale"); 
+	return false; 
+    } 
+
+    // select the input data sources
+    pmFPAfile *output = psMetadataLookupPtr (NULL, config->files, "PSASTRO.OUT.MODEL");
+    if (!output) psAbort ("PSASTRO.OUT.MODEL not listed in config->files");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE;
+    }
+    psLogMsg ("psastro", 3, "load headers : %f sec\n", psTimerMark ("psastro"));
+
+    // we should have a number of different files stored in config->files as PSASTRO.WCS
+    psMetadataItem *item = psMetadataLookup (config->files, "PSASTRO.WCS");
+    if (item == NULL) psAbort("missing PSASTRO.WCS entries in config->files");
+    if (item->type != PS_DATA_METADATA_MULTI) psAbort("unexpected type for PSASTRO.WCS");
+    psArray *files = psListToArray (item->data.list);
+
+    // convert the headers for the input file into fpa astrometry terms
+    for (int i = 0; i < files->n; i++) {
+	psMetadataItem *file = files->data[i];
+	pmFPAfile *input = file->data.V;
+
+	pmFPAviewReset (view);
+
+	// check PHU header to see if we are using mosaic-level or per-chip astrometry
+	bool bilevelAstrometry = false;
+	pmHDU *phu = pmFPAviewThisPHU (view, input->fpa);
+	if (phu) {
+	    char *ctype = psMetadataLookupStr (NULL, phu->header, "CTYPE1");
+	    if (ctype) bilevelAstrometry = !strcmp (&ctype[4], "-DIS");
+	}
+	if (bilevelAstrometry) {
+	    pmAstromReadBilevelMosaic (input->fpa, phu->header);
+	} 
+
+	while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+	    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+	    if (!chip->process || !chip->file_exists || !chip->data_exists) { continue; }
+
+	    // read WCS data from the corresponding header
+	    pmHDU *hdu = pmFPAviewThisHDU (view, input->fpa);
+	    int nAstro = psMetadataLookupS32 (&status, hdu->header, "NASTRO");
+	    if (!nAstro) continue;
+
+	    if (bilevelAstrometry) {
+		if (!pmAstromReadBilevelChip (chip, hdu->header)) {
+		    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		    continue;
+		} 
+	    } else {
+		if (!pmAstromReadWCS (input->fpa, chip, hdu->header, pixelScale)) {
+		    psWarning("Could not get WCS information from header for chip %d, skipping", view->chip); 
+		    continue;
+		} 
+	    }
+	}
+    }
+    psLogMsg ("psastro", 3, "convert wcs terms to internal format : %f sec\n", psTimerMark ("psastro"));
+
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataSave.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataSave.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelDataSave.c	(revision 22306)
@@ -0,0 +1,40 @@
+# include "psastroStandAlone.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+# define ESCAPE { \
+  psError(PS_ERR_UNKNOWN, false, "Failure in psastroModelDataSave"); \
+  psFree (view); \
+  return false; \
+}
+  
+bool psastroModelDataSave (pmConfig *config) {
+
+    bool status;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe");
+	return false;
+    }
+
+    pmFPAfile *output = psMetadataLookupPtr (&status, config->files, "PSASTRO.OUT.MODEL");
+    if (!status) psAbort ("Can't find output pmFPAfile PSASTRO.OUT.MODEL");
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmChip *chip = NULL;
+
+    while ((chip = pmFPAviewNextChip (view, output->fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process) { continue; }
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+    }
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE;
+
+    psLogMsg ("psastro", 3, "save headers : %f sec\n", psTimerMark ("psastro"));
+
+    return true;
+}
+
+
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFit.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFit.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFit.c	(revision 22306)
@@ -0,0 +1,37 @@
+# include "psastroStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+
+    if (argc != 3) {
+	fprintf (stderr, "USAGE: psastroModelFit (input) (output)\n");
+	exit (1);
+    }
+
+    FILE *f = fopen (argv[1], "r");
+    if (f == NULL) {
+	fprintf (stderr, "problem opening data file %s\n", argv[1]);
+	exit (2);
+    }
+
+    char name[1024];
+    psVector *Xo = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *Yo = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *Po = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    float x, y, p;
+
+    while (fscanf (f, "%s %f %f %f", name, &x, &y, &p) != EOF) {
+	psVectorAppend (Xo, x);
+	psVectorAppend (Yo, y);
+	psVectorAppend (Po, p*PS_RAD_DEG);
+    }	
+
+    // psTraceSetLevel("psLib.math", 5);
+    psastroModelFitBoresite (Xo, Yo, Po, argv[2]);
+
+    psLogMsg ("psastro", 3, "complete psastroModelFit run: %f sec\n", psTimerMark ("complete"));
+
+    exit (EXIT_SUCCESS);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFitBoresite.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFitBoresite.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelFitBoresite.c	(revision 22306)
@@ -0,0 +1,93 @@
+# include "psastroStandAlone.h"
+
+// we now have a set of observed L,M values.  fit these to the boresite model
+psVector *psastroModelFitBoresite (psVector *Xo, psVector *Yo, psVector *Po, char *outroot) {
+
+    assert (Xo->n > 2);
+    assert (Xo->n == Yo->n);
+    assert (Xo->n == Po->n);
+
+    // arrays to hold the data to be fitted
+    psArray *x = psArrayAlloc(2*Xo->n);
+    psVector *y = psVectorAlloc(2*Xo->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < Xo->n; i++) {
+
+	psVector *coord = NULL;
+
+	// X coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 0.0;
+	coord->data.F32[0] = Po->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = Xo->data.F32[i];
+	n++;
+	
+	// Y coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 1.0;
+	coord->data.F32[0] = Po->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = Yo->data.F32[i];
+	n++;
+    }	
+    assert (x->n == n);
+    assert (y->n == n);
+
+    psVector *params = psVectorAlloc (6, PS_TYPE_F32);
+    
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+
+    // XXX for now, no parameter masks, skip checkLimits
+    // constraint->checkLimits = psastroModelBoresiteLimits;
+
+    // make an initial guess:
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEAN | PS_STAT_MAX | PS_STAT_MIN);
+
+    // center (Xo) = mean(Xo), RX = range / 2
+    psVectorStats (stats, Xo, NULL, NULL, 0);
+    params->data.F32[PAR_X0] = stats->sampleMean;
+    params->data.F32[PAR_RX] = (stats->max - stats->min) / 2.0;
+
+    // center (Yo) = mean(Yo), RY = range / 2
+    psVectorStats (stats, Yo, NULL, NULL, 0);
+    params->data.F32[PAR_Y0] = stats->sampleMean;
+    params->data.F32[PAR_RY] = (stats->max - stats->min) / 2.0;
+
+    params->data.F32[PAR_P0] = 0.0;
+    params->data.F32[PAR_T0] = 0.0;
+
+    psMinimization *myMin = psMinimizationAlloc (25, 0.001);
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+    
+    // fprintf (stderr, "guess values:\n");
+    // fprintf (stderr, "Xo:  %f\n", params->data.F32[PAR_X0]);
+    // fprintf (stderr, "Yo:  %f\n", params->data.F32[PAR_Y0]);
+    // fprintf (stderr, "RX:  %f\n", params->data.F32[PAR_RX]);
+    // fprintf (stderr, "RY:  %f\n", params->data.F32[PAR_RY]);
+    // fprintf (stderr, "P0:  %f\n", params->data.F32[PAR_P0]);
+    // fprintf (stderr, "T0:  %f\n", params->data.F32[PAR_T0]);
+
+    // XXX skip the weights for now
+    psMinimizeLMChi2(myMin, covar, params, constraint, x, y, NULL, psastroModelBoresite);
+
+    char filename[256];
+    snprintf (filename, 256, "%s.pars", outroot);
+    FILE *outfile = fopen (filename, "w");
+    if (!outfile) {
+        psAbort("cannot open %s for output", filename);
+    }
+
+    fprintf (outfile, "# fitted values:\n");
+    fprintf (outfile, "Xo:  %f\n", params->data.F32[PAR_X0]);
+    fprintf (outfile, "Yo:  %f\n", params->data.F32[PAR_Y0]);
+    fprintf (outfile, "RX:  %f\n", params->data.F32[PAR_RX]);
+    fprintf (outfile, "RY:  %f\n", params->data.F32[PAR_RY]);
+    fprintf (outfile, "P0:  %f\n", params->data.F32[PAR_P0]);
+    fprintf (outfile, "T0:  %f\n", params->data.F32[PAR_T0]);
+    fclose (outfile);
+
+    return params;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelParseCamera.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelParseCamera.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroModelParseCamera.c	(revision 22306)
@@ -0,0 +1,44 @@
+# include "psastroInternal.h"
+
+bool psastroModelParseCamera (pmConfig *config) {
+
+    bool status = false;
+    pmFPAfile *input = NULL;
+
+    int nInput = psMetadataLookupS32 (&status, config->arguments, "INPUT.N");
+    if (!status) psAbort ("missing INPUT.N in config->arguments");
+
+    // find and load the input files (this determines the camera and builds the fpa structures,
+    // but does not load the data)
+    for (int i = 0; i < nInput; i++) {
+	char name[16];
+	snprintf (name, 16, "INPUT.%d", i);
+	input = pmFPAfileDefineFromArgs (&status, config, "PSASTRO.WCS", name);
+    }
+	
+    // set up an output fpa structure & file based on the last input file
+    pmFPAfile *output = pmFPAfileDefineOutput (config, input->fpa, "PSASTRO.OUT.MODEL");
+    if (!output) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA for PSASTRO.OUT.MODEL from input");
+	return false;
+    }
+    output->save = true;
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (output->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(output->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psTrace("psastro", 1, "Done with psastroModelParseCamera...\n");
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicAstrom.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicAstrom.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicAstrom.c	(revision 22306)
@@ -0,0 +1,151 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroMosaicFit (pmFPA *fpa, psMetadata *recipe, const char *rootname, int pass);
+
+// XXX require this fpa to have multiple chip extensions and a PHU?
+bool psastroMosaicAstrom (pmConfig *config) {
+
+    bool status;
+    char filename[256];
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find input data!\n");
+	return false;
+    }
+
+    pmFPA *fpa = input->fpa;
+
+    // before we do object matches, we need to (optionally) fix failed chips.  We compare chips with
+    // the supplied mosaic model.  Adjust significant outliers to match model.  
+    # if (0)
+    if (!psastroFixChips (config, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to align problematic chips");
+	return false;
+    }
+    if (!psastroFixChipsTest (config, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to align problematic chips");
+	return false;
+    }
+    # endif
+
+    char *outroot = psMetadataLookupStr (&status, config->arguments, "OUTPUT");
+    if (!status || !outroot) psAbort ("Can't find outroot on config->arguments");
+
+    if (!psastroMosaicFit (fpa, recipe, outroot, 0)) return false;
+    if (!psastroMosaicFit (fpa, recipe, outroot, 1)) return false;
+    if (!psastroMosaicFit (fpa, recipe, outroot, 2)) return false;
+    if (!psastroMosaicFit (fpa, recipe, outroot, 3)) return false;
+
+    // now fit the chips under the common distortion with higher-order terms
+    // first, re-perform the match with a slightly tighter circle
+    if (!psastroMosaicSetMatch (fpa, recipe, 4)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (4th pass)");
+	return false;
+    }
+    if (!psastroMosaicChipAstrom (fpa, recipe, 4)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (4th pass)");
+	return false;
+    }
+    if (psTraceGetLevel("psastro.dump") > 0) { 
+	snprintf (filename, 256, "%s.10.dat", outroot);
+	psastroDumpMatches (fpa, filename); 
+    }
+
+    // save WCS and analysis metadata in update header.
+    psMetadata *updates = psMetadataAlloc();
+    if (!pmAstromWriteBilevelMosaic (updates, fpa, NONLIN_TOL)) {
+	psAbort ("failed to save header terms");
+    }
+
+    // write the elapsed time here; this will be updated in psastroMosaicAstrometry, if called
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "DT_ASTR", PS_META_REPLACE, "elapsed psastro time", psTimerMark ("psastroAnalysis"));
+
+    psMetadataAddMetadata (fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+    psFree (updates);
+
+    // update the headers based on the results
+    // XXX need to add global summary statistics
+    // psastroMosaicHeaders (config);
+
+    return true;
+}
+
+/* coordinate frame hierachy
+ * pixels (on a given readout)
+ * cell
+ * chip
+ * FP (focal plane)
+ * TP (tangent plane)
+ * sky (ra, dec)
+ */
+
+// 1: match 4,5
+// 2: match 6,7
+// 3: match 8,9
+bool psastroMosaicFit (pmFPA *fpa, psMetadata *recipe, const char *rootname, int pass) {
+
+    char filename[256];
+
+    // given the existing per-chip astrometry, determine matches between raw and ref stars
+    // is this needed? yes, if we didn't do SingleChip astrometry first
+    if (!psastroMosaicSetMatch (fpa, recipe, pass)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic (pass %d)", pass);
+	return false;
+    }
+
+    if ((pass == 0) && (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1)) { 
+	snprintf (filename, 256, "%s.0.dat", rootname);
+	psastroDumpMatches (fpa, filename); 
+    }
+
+    // fitted chips will follow the local plate-scale, hiding the distortion
+    // modify the chip->toFPA scaling to match knowledge about pixel scale,
+    // then recalculate raw and ref positions
+    if (!psastroMosaicCommonScale (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to set a common scale for the chips (pass %d)", pass);
+	return false;
+    }
+
+    if ((pass == 0) && (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1)) { 
+	snprintf (filename, 256, "%s.1.dat", rootname);
+	psastroDumpMatches (fpa, filename); 
+    }
+
+    // fit the distortion by fitting its gradient
+    // apply the new distortion terms up and down
+    // refit the per-chip terms with linear fits only
+    if (!psastroMosaicDistortion (fpa, recipe, pass)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure mosaic gradients (pass %d)", pass);
+	return false;
+    }
+
+    snprintf (filename, 256, "%s.%d.dat", rootname, 2*pass + 2);
+    if (psTraceGetLevel("psastro.dump.psastroMosaicAstrom") > 1) { psastroDumpMatches (fpa, filename); }
+    
+    // measure the astrometry for the chips under the distortion term
+    if (!psastroMosaicChipAstrom (fpa, recipe, pass)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to measure chip astrometry in mosaic mode (pass %d)", pass);
+	return false;
+    }
+
+    int traceLevel = psTraceGetLevel("psastro.dump.psastroMosaicAstrom");
+    if (traceLevel > 1) {
+	snprintf (filename, 256, "%s.%d.dat", rootname, 2*pass + 3);
+	psastroDumpMatches (fpa, filename); 
+    } else {
+	snprintf (filename, 256, "%s.dat", rootname);
+	psastroDumpMatches (fpa, filename); 
+    }
+
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicChipAstrom.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicChipAstrom.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicChipAstrom.c	(revision 22306)
@@ -0,0 +1,47 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+
+bool psastroMosaicChipAstrom (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+        if (!chip->toFPA) { continue; }
+
+        while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+            // process each of the readouts
+            // XXX there can only be one readout per chip, right?
+            while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+                if (! readout->data_exists) { continue; }
+
+                // save WCS and analysis metadata in update header
+                psMetadata *updates = psMetadataAlloc();
+
+                if (!psastroMosaicOneChip (chip, readout, recipe, updates, iteration)) {
+                    readout->data_exists = false;
+                    psError(PS_ERR_UNKNOWN, false, "failed to find a solution for %d,%d,%d\n",
+                            view->chip, view->cell, view->readout);
+                    psFree(updates);
+                    psFree(view);
+                    return false;
+                }
+
+                // create the header keywords to descripe the results
+                pmAstromWriteBilevelChip (updates, chip, NONLIN_TOL);
+                psMetadataAddMetadata (readout->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+                psFree (updates);
+            }
+        }
+    }
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicCorrectDistortion.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicCorrectDistortion.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicCorrectDistortion.c	(revision 22306)
@@ -0,0 +1,81 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicCorrectDistortion (pmFPA *fpa, psPlaneTransform *TPtoFP) {
+
+    // invert the linear TPtoFP transform
+    psPlaneTransform *FPtoTP = p_psPlaneTransformLinearInvert (TPtoFP);
+    if (FPtoTP == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert TPtoFP\n");
+        return false;
+    }
+
+    // store the new coeffs in a new structure
+    psPlaneTransform *toTPAnew = psPlaneTransformAlloc(fpa->toTPA->x->nX, fpa->toTPA->x->nY);
+
+    // set the new coeffs, or set the mask
+    for (int i = 0; i <= toTPAnew->y->nX; i++) {
+        for (int j = 0; j <= toTPAnew->y->nY; j++) {
+
+	    // init the coeffs
+	    toTPAnew->x->coeff[i][j] = 0.0;
+	    toTPAnew->y->coeff[i][j] = 0.0;
+
+	    // if both are masked, mask the outpu
+            if ((fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) && (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET)) {
+		toTPAnew->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		toTPAnew->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		continue;
+	    } 
+
+	    // set the X terms
+	    toTPAnew->x->coeff[i][j] += (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->x->coeff[1][0] * fpa->toTPA->x->coeff[i][j];
+	    toTPAnew->x->coeff[i][j] += (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->x->coeff[0][1] * fpa->toTPA->y->coeff[i][j];
+
+	    // set the Y terms
+	    toTPAnew->y->coeff[i][j] += (fpa->toTPA->x->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->y->coeff[1][0] * fpa->toTPA->x->coeff[i][j];
+	    toTPAnew->y->coeff[i][j] += (fpa->toTPA->y->coeffMask[i][j] & PS_POLY_MASK_SET) ? 0.0 : FPtoTP->y->coeff[0][1] * fpa->toTPA->y->coeff[i][j];
+        }
+    }
+
+    // adjust the 0,0 terms:
+    toTPAnew->x->coeff[0][0] += FPtoTP->x->coeff[0][0];
+    toTPAnew->y->coeff[0][0] += FPtoTP->y->coeff[0][0];
+
+    psFree (fpa->toTPA);
+    fpa->toTPA = toTPAnew;
+
+    // invert toTPA to determine fromTPA. choose an appropriate region based on the dimensions
+    // of the complete FPA
+    psRegion *region = pmAstromFPAExtent (fpa);
+
+    psFree (fpa->fromTPA);
+    fpa->fromTPA = psPlaneTransformInvert(NULL, fpa->toTPA, *region, 50);
+    psFree (region);
+
+    if (fpa->fromTPA == NULL) {
+        psError (PS_ERR_UNKNOWN, false, "failed to invert fpa->toTPA\n");
+	psFree (FPtoTP);
+        return false;
+    }
+
+    psFree (FPtoTP);
+    return true;
+}
+
+// we have three coordinate systems and two polynomial transformations:
+// TP is the raw tangent plane coordinate system (aligned with RA,DEC)
+// FP is the raw focal plane coordinate system (aligned with camera X,Y)
+// dFP is the distorted focal plane coordinate system
+
+// fpa->toTPA is a polynomial transformation from FP to dFP
+//   L(x,y) = \sum_i \sum_j A_{i,j} x^i y^j and 
+//   M(x,y) = \sum_i \sum_j B_{i,j} x^i y^j 
+//   where (x,y) are the FP coords and L,M  are the dFP coords
+
+// FPtoTP is a linear transformation from dFP to TP (we recover this from TPtoFP by inversion)
+// P(L,M) = r_xo + r_xx L + r_xy M
+// Q(L,M) = r_yo + r_yx L + r_yy M
+
+// we need to merge them into a single transformation:
+// P(x,y) = r_xo + r_xx * \sum_i \sum_j A_{i,j} x^i y^j + r_xy * \sum_i \sum_j B_{i,j} x^i y^j 
+// Q(x,y) = r_yo + r_yx * \sum_i \sum_j A_{i,j} x^i y^j + r_yy * \sum_i \sum_j B_{i,j} x^i y^j 
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicDistortion.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicDistortion.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicDistortion.c	(revision 22306)
@@ -0,0 +1,55 @@
+# include "psastroInternal.h"
+# define DEBUG 0
+
+bool psastroMosaicDistortion (pmFPA *fpa, psMetadata *recipe, int pass) {
+
+    // fit a linear transformation from reference TP to observed FP coords
+    psPlaneTransform *TPtoFP = psastroMosaicFitRotAndScale (fpa);
+    if (!TPtoFP) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit a linear TP correction\n");
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v1.dat", "corners.dn.v1.dat", fpa);
+
+    // Correct the current reference star TP coordinates to the nearly-FP
+    // system.  We note two points here: 1) the corrected TP coordinates are
+    // NOT consistent with the sky coordinates, 2) the remaining differnce
+    // between the new TP reference coords and the observerd FP coordinates is
+    // only distortion
+    if (!psastroMosaicApplyRotAndScale (fpa, TPtoFP)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply the linear TP correction\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v2.dat", "corners.dn.v2.dat", fpa);
+
+    if (!psastroMosaicDistortionFromGradients (fpa, recipe)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit the distortion field\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v3.dat", "corners.dn.v3.dat", fpa);
+
+    if (!psastroMosaicCorrectDistortion (fpa, TPtoFP)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to correct the distortion for the linear fit\n");
+	psFree (TPtoFP);
+        return false;
+    }
+	
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v4.dat", "corners.dn.v4.dat", fpa);
+
+    if (!psastroMosaicSetAstrom (fpa)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to apply mosaic distortion terms\n");
+	psFree (TPtoFP);
+        return false;
+    }
+
+    if (DEBUG && (pass == 0)) psastroDumpCorners ("corners.up.v5.dat", "corners.dn.v5.dat", fpa);
+
+    psFree (TPtoFP);
+    return true;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicFPtoTP.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicFPtoTP.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicFPtoTP.c	(revision 22306)
@@ -0,0 +1,164 @@
+# include "psastroInternal.h"
+
+/************************************************/
+psPlaneTransform *psastroMosaicFitRotAndScale (pmFPA *fpa) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // XXX first pass: fit and remove any linear fp->tp transformation
+    // accumulate FP(x,y) & TP(x,y) in a single set of vectors
+
+    psVector *X  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *Y  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *x  = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *y  = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // int nPts = 0;
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// take the matched stars, first fit
+		for (int i = 0; i < match->n; i++) {
+		    pmAstromMatch *pair = match->data[i];
+		    pmAstromObj *rawStar = rawstars->data[pair->raw];
+		    pmAstromObj *refStar = refstars->data[pair->ref];
+
+		    // independent variables
+		    X->data.F32[X->n] = refStar->TP->x;
+		    Y->data.F32[Y->n] = refStar->TP->y;
+
+		    // fitted values
+		    x->data.F32[x->n] = rawStar->FP->x;
+		    y->data.F32[y->n] = rawStar->FP->y;
+
+		    psVectorExtend (X, 100, 1);
+		    psVectorExtend (Y, 100, 1);
+		    psVectorExtend (x, 100, 1);
+		    psVectorExtend (y, 100, 1);
+		}
+	    }
+	}
+    }
+    // x->n = y->n = X->n = Y->n = nPts;
+
+    // linear fit without xy cross term
+    psPlaneTransform *map = psPlaneTransformAlloc (1, 1);
+    map->x->coeffMask[1][1] = PS_POLY_MASK_SET;
+    map->y->coeffMask[1][1] = PS_POLY_MASK_SET;
+
+    // constant errors
+    psVector *mask = psVectorAlloc (X->n, PS_TYPE_U8);
+    psVectorInit (mask, 0);
+
+    // the stats options supplied are used to perform the clip fitting
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    stats->clipIter = 1;
+
+    // fit TP-to-FP transformation
+
+    // we run 3 cycles clipping in each of x and y, with only one iteration each.
+    // XXX use the stats lookups functions to get the width and center
+    for (int i = 0; i < 3; i++) {
+	if (!psVectorClipFitPolynomial2D (map->x, stats, mask, 0xff, x, NULL, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for x\n");
+	    psFree (map);
+	    map = NULL;
+	    goto escape;
+	}
+        psTrace ("psastro", 3, "x resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, x->n);
+
+        if (!psVectorClipFitPolynomial2D (map->y, stats, mask, 0xff, y, NULL, X, Y)) {
+            psError(PS_ERR_UNKNOWN, false, "failure in clip-fitting for y\n");
+	    psFree (map);
+	    map = NULL;
+	    goto escape;
+	}
+        psTrace ("psastro", 3, "y resid: %f +/- %f (%ld of %ld)\n", stats->clippedMean, stats->clippedStdev, stats->clippedNvalues, y->n);
+    }
+
+escape:
+    psFree (x);
+    psFree (y);
+    psFree (X);
+    psFree (Y);
+    psFree (mask);
+    psFree (view);
+    psFree (stats);
+
+    return (map);
+}
+
+/*****************************/
+bool psastroMosaicApplyRotAndScale (pmFPA *fpa, psPlaneTransform *TPtoFP) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    psPlane newTP;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		// take the matched stars, first fit
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *refStar = refstars->data[i];
+
+		    // Correct the current reference star TP coordinates to the nearly-FP
+		    // system.  We note two points here: 1) the corrected TP coordinates are
+		    // NOT consistent with the sky coordinates, 2) the remaining differnce
+		    // between the new TP reference coords and the observerd FP coordinates is
+		    // only distortion
+
+		    psPlaneTransformApply (&newTP, TPtoFP, refStar->TP);
+		    refStar->TP->x = newTP.x;
+		    refStar->TP->y = newTP.y;
+		}
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGetGrads.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGetGrads.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGetGrads.c	(revision 22306)
@@ -0,0 +1,48 @@
+# include "psastroInternal.h"
+
+psArray *psastroMosaicGetGrads (pmFPA *fpa, psMetadata *recipe) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *grads = NULL;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// the new elements are added to the incoming gradient structure 
+		grads = pmAstromMeasureGradients (grads, rawstars, refstars, match, region, 2, 2);
+	    }
+	}
+	psFree (region);
+    }
+    psFree (view);
+    return (grads);
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGradients.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGradients.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicGradients.c	(revision 22306)
@@ -0,0 +1,106 @@
+# include "psastroInternal.h"
+static int nPass = 0;
+
+bool psastroMosaicDistortionFromGradients (pmFPA *fpa, psMetadata *recipe) {
+
+    bool status;
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    psArray *gradients = NULL;
+
+    // Measure the gradient as a function of position.  This must be performed between the
+    // corrected ref->TP and the observed raw->FP, for which the distortion is a perturbation.
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    int nXcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NX");
+    int nYcell = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.GRADIENT.NY");
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	psRegion *region = pmChipExtent (chip);
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (match == NULL) { continue; }
+
+		// measure the local gradients for this set of stars
+		// XXX 2,2 are the number of test boxes on the chip.  put this in the recipe
+		gradients = pmAstromMeasureGradients (gradients, rawstars, refstars, match, region, nXcell, nYcell);
+	    }
+	}
+	psFree (region);
+    }
+
+    // if desired, dump the gradients to a file
+    if (psTraceGetLevel("psastro.dump") > 0) { 
+	char name[80];
+	sprintf (name, "gradients.%d.dat", nPass); 
+	psastroDumpGradients (gradients, name); 
+	nPass ++;
+    }
+
+    // Fit the gradient field and convert to the distortion terms.
+
+    // allocate mosaic-level polynomial transformation and set masks needed by DVO
+    int order = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.ORDER");
+    if (!status) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to find single-chip fit order\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+    psFree (fpa->toTPA);
+    fpa->toTPA = psPlaneTransformAlloc (order, order);
+    for (int i = 0; i <= fpa->toTPA->x->nX; i++) {
+        for (int j = 0; j <= fpa->toTPA->x->nY; j++) {
+            if (i + j > order) {
+		fpa->toTPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		fpa->toTPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+            }
+        }
+    }
+
+    // physical pixel scale in microns per pixel (FP is in physical units, chip is in pixels)
+    double pixelScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PIXEL.SCALE");
+    if (!status) {
+	psError(PS_ERR_IO, false, "Failed to lookup pixel scale"); 
+	psFree (gradients);
+	psFree (view);
+	return false; 
+    } 
+
+    // fit the measured gradients with the telescope distortion model (polynomial order based on toTPA)
+    if (!pmAstromFitDistortion (fpa, gradients, pixelScale)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to fit the distortion terms\n");
+	psFree (gradients);
+	psFree (view);
+        return false;
+    }
+	
+    psFree (gradients);
+    psFree (view);
+    return true;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicHeaders.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicHeaders.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicHeaders.c	(revision 22306)
@@ -0,0 +1,59 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicHeaders (pmConfig *config) {
+
+    bool status = false;
+    pmChip *chip = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+ 	psError(PSASTRO_ERR_CONFIG, true, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, true, "Can't find input data!\n");
+	return false;
+    }
+
+    char *mosastro = psMetadataLookupStr (NULL, config->arguments, "MOSASTRO");
+
+    double plateScale = psMetadataLookupF32 (&status, recipe, "PSASTRO.PLATE.SCALE");
+    if (!status) plateScale = 1.0;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmFPA *fpa = input->fpa;
+
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+
+        // read WCS data from the corresponding header
+        pmHDU *hdu = pmFPAviewThisHDU (view, fpa);
+
+        pmAstromWriteBilevelChip (chip->toFPA, hdu->header, plateScale);
+    }
+
+    psMetadata *mosaic = pmAstromWriteBilevelMosaic (fpa->toSky, fpa->toTPA, plateScale);
+
+    // XXX what is the EXTNAME??
+    psFits *fits = psFitsOpen (mosastro, "w");
+    psFitsWriteBlank(fits, mosaic, "");
+    psFitsClose (fits);
+
+    psFree (view);
+    return true;
+}
+
+/* coordinate frame hierachy
+   pixels (on a given readout)
+   cell
+   chip
+   FP (focal plane)
+   TP (tangent plane)
+   sky (ra, dec)
+*/
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicOneChip.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicOneChip.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicOneChip.c	(revision 22306)
@@ -0,0 +1,152 @@
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE, MESSAGE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psError(PSASTRO_ERR_CONFIG, false, MESSAGE); \
+   return false; } 
+
+bool psastroMosaicOneChip (pmChip *chip, pmReadout *readout, psMetadata *recipe, psMetadata *updates, int iteration) {
+
+    bool status;
+    char errorWord[64];
+    char orderWord[64];
+
+    PS_ASSERT_PTR_NON_NULL(chip,    false);
+    PS_ASSERT_PTR_NON_NULL(readout, false);
+    PS_ASSERT_PTR_NON_NULL(recipe,  false);
+    PS_ASSERT_PTR_NON_NULL(updates, false);
+
+    // select the raw objects for this readout
+    psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+    if (rawstars == NULL) return false;
+
+    psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+    if (refstars == NULL) return false;
+
+    psArray *match = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+    if (match == NULL) return false;
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32, "Failed to lookup pixel scale"); 
+
+    // allowed limits for valid solutions
+    snprintf (errorWord, 64, "PSASTRO.MOSAIC.MAX.ERROR.N%d", iteration);
+    REQUIRED_RECIPE_VALUE (float maxError, errorWord, F32, "failed to find single-chip max allowed error\n");
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MOSAIC.MIN.NSTAR", S32, "failed to find single-chip min allowed stars\n");
+
+    // set the order of the per-chip fit (higher order only if iteration > 0)
+    REQUIRED_RECIPE_VALUE (int defaultOrder, "PSASTRO.MOSAIC.CHIP.ORDER", S32, "failed to find mosaic chip-level fit default order\n");
+
+    snprintf (orderWord, 64, "PSASTRO.MOSAIC.CHIP.ORDER.N%d", iteration);
+    int order = psMetadataLookupS32 (&status, recipe, orderWord);
+    if (!status || (order == -1)) {
+	order = defaultOrder;
+    }
+
+    // modify the order to correspond to the actual number of matched stars:
+    if ((match->n < 15) && (order >= 3)) order = 2;
+    if ((match->n < 11) && (order >= 2)) order = 1;
+    if ((match->n <  8) && (order >= 1)) order = 0;
+    if ((match->n <  3) || (order < 0) || (order > 3)) {
+	psLogMsg ("psastro", 3, "insufficient stars (%ld) or invalid order (%d)", match->n, order); 
+	return false; 
+    } 
+
+    psLogMsg ("psastro", PS_LOG_DETAIL, "mosaic fit chip order %d", order);
+
+    // create output toFPA; set masks appropriate to the Elixir DVO astrometry format if we are
+    // fitting 0th order, use the current polynomial of whatever order, with higher order
+    // coefficients frozen to the current values
+    if (order == 0) {
+	// set FIT mask for all higher order terms of the existing solution
+	// any existing SET masks will be retained.
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > 0) {
+		    chip->toFPA->x->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+		    chip->toFPA->y->coeffMask[i][j] |= PS_POLY_MASK_FIT;
+		}
+	    }
+	}
+    } else {
+	psFree (chip->toFPA);
+	chip->toFPA = psPlaneTransformAlloc (order, order);
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > order) {
+		    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		}
+	    }
+	}
+    }
+
+    // XXX allow statitic to be set by the user
+    // only clip if we are fitting the chip parameters.
+    psStats *fitStats = NULL;
+//    if (order == 0) {
+//	fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+//	fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NSIGMA");
+//	fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+//    } else {
+	fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+	fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NSIGMA");
+	fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.MOSAIC.CHIP.NITER");
+//    }
+
+    // need to pass in an update header, sent in from above
+    pmAstromFitResults *results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats);
+    if (!results) {
+	psError(PSASTRO_ERR_DATA, false, "failed to perform the matched fit\n");
+	return false;
+    }
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    pmFPA *fpa = chip->parent;
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    float pixError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+    int astNstar = results->yStats->clippedNvalues;
+
+    // if we clip away too many stars, the order may be invalid
+    if (order == 3) { minNstar = PS_MAX (15, minNstar); }
+    if (order == 2) { minNstar = PS_MAX (11, minNstar); }
+    if (order == 1) { minNstar = PS_MAX ( 8, minNstar); }
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if ((maxError > 0) && (astError > maxError)) {
+	psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+	validSolution = false;
+    }
+    if (astNstar < minNstar) {
+	psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+	validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // determine fromFPA transformation and apply new transformation to raw & ref stars
+    psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+
+    psFree (fitStats);
+    psFree (results);
+    return validSolution;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetAstrom.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetAstrom.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetAstrom.c	(revision 22306)
@@ -0,0 +1,52 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicSetAstrom (pmFPA *fpa) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		for (int i = 0; i < rawstars->n; i++) {
+		    pmAstromObj *raw = rawstars->data[i];
+	
+		    psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+		    psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+		    psDeproject (raw->sky, raw->TP, fpa->toSky);
+		}
+
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		for (int i = 0; i < refstars->n; i++) {
+		    pmAstromObj *ref = refstars->data[i];
+	
+		    psProject (ref->TP, ref->sky, fpa->toSky);
+		    psPlaneTransformApply (ref->FP, fpa->fromTPA, ref->TP);
+		    psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+		}
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetMatch.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetMatch.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroMosaicSetMatch.c	(revision 22306)
@@ -0,0 +1,67 @@
+# include "psastroInternal.h"
+
+bool psastroMosaicSetMatch (pmFPA *fpa, psMetadata *recipe, int iteration) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+    pmFPAview *view = pmFPAviewAlloc (0);
+    char radiusWord[64];
+
+    // use small radius to match stars (assume starting astrometry is good)
+    bool status = false; 
+    sprintf (radiusWord, "PSASTRO.MOSAIC.RADIUS.N%d", iteration);
+    double RADIUS = psMetadataLookupF32 (&status, recipe, radiusWord); 
+    if (!status) { 
+	psError(PS_ERR_IO, false, "Failed to lookup matching radius: %s", radiusWord); 
+	psFree (view);
+	return false; 
+    } 
+
+    if (RADIUS <= 0.0) {
+	if (iteration == 0) {
+	    psError(PS_ERR_IO, false, "Invalid match radius for first iteration: %s", radiusWord); 
+	    psFree (view);
+	    return false; 
+	} 
+	psWarning ("skipping match for iteration %d\n", iteration);
+	psFree (view);
+	return true;
+    }
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->fromFPA) { continue; }
+	
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+		psTrace ("psastro", 4, "Trying %ld refstars\n", refstars->n);
+
+		psArray *matches = pmAstromRadiusMatchChip (rawstars, refstars, RADIUS);
+		psTrace ("psastro", 4, "Matched %ld refstars\n", matches->n);
+
+		// XXX drop the old one
+		psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.MATCH", PS_DATA_ARRAY | PS_META_REPLACE, "astrometry matches", matches);
+		psFree (matches);
+	    }
+	}
+    }
+    psFree (view);
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipFit.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipFit.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipFit.c	(revision 22306)
@@ -0,0 +1,166 @@
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psAbort ("Failed to find %s in recipe", NAME); }
+
+bool psastroOneChipFit (pmFPA *fpa, pmChip *chip, psArray *refstars, psArray *rawstars, psMetadata *recipe, psMetadata *updates) {
+
+    bool status;
+
+    // default value for match/fit : radius is in pixels
+    REQUIRED_RECIPE_VALUE (double RADIUS, "PSASTRO.MATCH.RADIUS", F32); 
+
+    // run the match/fit sequence NITER times
+    REQUIRED_RECIPE_VALUE (int nIter, "PSASTRO.MATCH.FIT.NITER", S32); 
+
+    // correct radius to FP units (physical pixel scale in microns per pixel)
+    REQUIRED_RECIPE_VALUE (double pixelScale, "PSASTRO.PIXEL.SCALE", F32); 
+    RADIUS *= pixelScale;
+
+    // select the desired chip order
+    REQUIRED_RECIPE_VALUE (int order, "PSASTRO.CHIP.ORDER", S32);
+
+    // allowed limits for valid solutions
+    REQUIRED_RECIPE_VALUE (float maxError, "PSASTRO.MAX.ERROR", F32);
+    REQUIRED_RECIPE_VALUE (int minNstar, "PSASTRO.MIN.NSTAR", S32);
+
+    psArray *match = NULL;
+    psStats *fitStats = NULL;
+    pmAstromFitResults *results = NULL;
+
+    for (int iter = 0; iter < nIter; iter++) {
+	
+	char name[128];
+
+	sprintf (name, "PSASTRO.MATCH.RADIUS.N%d", iter);
+	float radius = psMetadataLookupF32 (&status, recipe, name);
+	radius *= pixelScale;
+	if (!status || (radius == 0.0)) {
+	    radius = RADIUS;
+	}
+
+
+	// use small radius to match stars
+	match = pmAstromRadiusMatchFP (rawstars, refstars, radius);
+	if (match == NULL) {
+	    psLogMsg ("psastro", 3, "failed to find radius-matched sources\n");
+	    return false;
+	}
+
+	// modify the order to correspond to the actual number of matched stars:
+	if ((match->n < 11) && (order >= 3)) order = 2;
+	if ((match->n <  7) && (order >= 2)) order = 1;
+	if ((match->n <  4) && (order >= 1)) order = 0;
+	if (order < 1) {
+	    psLogMsg ("psastro", 3, "insufficient stars or invalid order: %ld stars", match->n); 
+	    psFree (match);
+	    return false; 
+	} 
+
+	// create output toFPA; set masks appropriate to the Elixir DVO astrometry format
+	psFree (chip->toFPA);
+	chip->toFPA = psPlaneTransformAlloc (order, order);
+	for (int i = 0; i <= chip->toFPA->x->nX; i++) {
+	    for (int j = 0; j <= chip->toFPA->x->nY; j++) {
+		if (i + j > order) {
+		    chip->toFPA->x->coeffMask[i][j] = PS_POLY_MASK_SET;
+		    chip->toFPA->y->coeffMask[i][j] = PS_POLY_MASK_SET;
+		}
+	    }
+	}
+
+	// XXX allow statitic to be set by the user
+	// fitStats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+	fitStats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+	fitStats->clipSigma = psMetadataLookupF32 (&status, recipe, "PSASTRO.CHIP.NSIGMA");
+	fitStats->clipIter = psMetadataLookupS32 (&status, recipe, "PSASTRO.CHIP.NITER");
+
+	// improved fit for astrometric terms
+	results = pmAstromMatchFit (chip->toFPA, rawstars, refstars, match, fitStats);
+	if (!results) {
+	    psLogMsg ("psastro", 3, "failed to perform the matched fit\n");
+	    psFree (match);
+	    psFree (fitStats);
+	    return false;
+	}
+    
+	// determine fromFPA transformation and apply new transformation to raw & ref stars
+	psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+    
+	// toSky converts from FPA & TPA units (microns) to sky units (radians)
+	float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+	// float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+	float astError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) * plateScale;
+	int astNstar = results->yStats->clippedNvalues;
+	psLogMsg ("psastro", PS_LOG_INFO, "pass %d, error: %f arcsec, Nstars: %d", iter, astError, astNstar);
+
+	if (iter < nIter - 1) {
+	    psFree (fitStats);
+	    psFree (results);
+	    psFree (match);
+	}
+    }
+
+    // toSky converts from FPA & TPA units (microns) to sky units (radians)
+    float plateScale = 0.5*(fpa->toSky->Xs + fpa->toSky->Ys)*3600.0*PM_DEG_RAD;
+
+    // pixError is the average 1D scatter in pixels ('results' are in FPA units = microns)
+    // float pixError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) / pixelScale;
+    float pixError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) / pixelScale;
+
+    // astError is the average 1D scatter in arcsec ('results' are in FPA units = microns)
+    // float astError = 0.5*(results->xStats->clippedStdev + results->yStats->clippedStdev) * plateScale;
+    float astError = 0.5*(results->xStats->robustStdev + results->yStats->robustStdev) * plateScale;
+    int astNstar = results->yStats->clippedNvalues;
+
+    bool validSolution = true;
+
+    // XXX should these result in errors or be handled another way?
+    psLogMsg ("psastro", PS_LOG_INFO, "astrometry solution: error: %f arcsec, Nstars: %d", astError, astNstar);
+    if (astError > maxError) {
+        psLogMsg("psastro", PS_LOG_INFO, "residual error is too large, failed to find a solution: %f > %f", astError, maxError);
+	validSolution = false;
+    }
+    if (astNstar < minNstar) {
+        psLogMsg("psastro", PS_LOG_INFO, "solution uses too few stars: %d < %d", astNstar, minNstar);
+	validSolution = false;
+    }
+
+    // DVO expects NASTRO = 0 if we fail to find a solution
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "PERROR",   PS_META_REPLACE, "astrometry error (pixels)", pixError);
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "CERROR",   PS_META_REPLACE, "astrometry error (arcsec)", astError);
+    if (validSolution) {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", astError/sqrt(astNstar));
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", astNstar);
+    } else {
+	psMetadataAddF32 (updates, PS_LIST_TAIL, "CPRECISE", PS_META_REPLACE, "astrometry precision (arcsec)", 0.0);
+	psMetadataAddS32 (updates, PS_LIST_TAIL, "NASTRO",   PS_META_REPLACE, "number of astrometry stars", 0);
+    }
+    psMetadataAddF32 (updates, PS_LIST_TAIL, "EQUINOX",  PS_META_REPLACE, "equinox of ref catalog", 2000.0); // XXX this is bogus: should be defined based on equinox of refstars
+
+    // XXX drop from here : determine fromFPA transformation and apply new transformation to raw & ref stars
+    // psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+    
+    // XXX check if we correctly applied the new transformation:
+    if (psTraceGetLevel("psastro.dump") > 0) {
+	psastroDumpRawstars (rawstars, fpa, chip);
+	psastroDumpMatchedStars ("match.dat", rawstars, refstars, match);
+	psastroDumpStars (refstars, "refstars.cal.dat");
+    }
+
+    if (psTraceGetLevel("psastro.plot") > 0) {
+	psastroPlotOneChipFit (rawstars, refstars, match, recipe);
+    }
+
+    psFree (match);
+    psFree (results);
+    psFree (fitStats);
+    return validSolution;
+}
+
+// psastroWriteStars ("raw.1.dat", rawstars);
+// psastroWriteStars ("ref.1.dat", refstars);
+// psastroWriteTransform (chip->toFPA);
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipGrid.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipGrid.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroOneChipGrid.c	(revision 22306)
@@ -0,0 +1,64 @@
+# include "psastroInternal.h"
+
+# define REQUIRED_RECIPE_VALUE(VALUE, NAME, TYPE)\
+  VALUE = psMetadataLookup##TYPE (&status, recipe, NAME); \
+  if (!status) { \
+   psAbort ("Failed to find %s in recipe", NAME); }
+
+bool psastroOneChipGrid (pmFPA *fpa, pmChip *chip, psArray *refstars, psArray *rawstars, psMetadata *recipe, psMetadata *updates) {
+
+    bool status;
+    pmAstromStats *stats = NULL;
+
+    // do we need to get a rough initial match?
+    REQUIRED_RECIPE_VALUE (bool gridSearch, "PSASTRO.GRID.SEARCH", Bool);
+    if (!gridSearch) return true;
+
+    // do we need to get a rough initial match?
+    REQUIRED_RECIPE_VALUE (int maxNstar, "PSASTRO.GRID.NSTAR.MAX", S32);
+
+    // generate the bright subset of maxNstar entries (note: rawstars is sorted by S/N)
+    psArray *subset = psArrayAlloc (PS_MIN (maxNstar, rawstars->n));
+    for (int i = 0; (i < maxNstar) && (i < rawstars->n); i++) {
+	subset->data[i] = psMemIncrRefCounter (rawstars->data[i]);
+    }
+
+    // XXX set clump scale from recipe
+    psArray *gridStars = psastroRemoveClumps (subset, 150);
+    psFree (subset);
+
+    psArray *refSubset = psastroRemoveClumps (refstars, 150);
+
+    psLogMsg ("psastro", 3, "grid search using %ld raw vs %ld ref stars\n", gridStars->n, refSubset->n);
+
+    // find initial offset / rotation / scale
+    pmAstromStats *gridStats = pmAstromGridMatch (gridStars, refSubset, recipe);
+    if (gridStats == NULL) {
+	psLogMsg ("psastro", 3, "failed to find a grid match solution\n");
+	psFree (gridStars);
+	psFree (refSubset);
+	return false;
+    }
+    psLogMsg ("psastro", 3, "basic grid search result - offset: %f,%f pixels, rotation: %f deg\n", gridStats->offset.x, gridStats->offset.y, DEG_RAD*gridStats->angle);
+
+    // tweak the position by finding peak of matches stars
+    stats = pmAstromGridTweak (gridStars, refSubset, recipe, gridStats);
+    if (stats == NULL) {
+	psLogMsg ("psastro", 3, "failed to measure tweaked grid solution\n");
+	psFree (gridStats);
+	psFree (gridStars);
+	psFree (refSubset);
+	return false;
+    }
+    psLogMsg ("psastro", 3, "tweak grid search result - offset: %f,%f pixels, rotation: %f deg\n", stats->offset.x, stats->offset.y, DEG_RAD*stats->angle);
+
+    // adjust the chip.toFPA terms only
+    pmAstromGridApply (chip->toFPA, stats);
+    psastroUpdateChipToFPA (fpa, chip, rawstars, refstars);
+    psFree (gridStats);
+    psFree (gridStars);
+    psFree (refSubset);
+    psFree (stats);
+
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroParseCamera.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroParseCamera.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroParseCamera.c	(revision 22306)
@@ -0,0 +1,46 @@
+# include "psastroInternal.h"
+
+bool psastroParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the input image(s) are required arguments; they define the camera
+    pmFPAfile *input = pmFPAfileDefineFromArgs (&status, config, "PSASTRO.INPUT", "INPUT");
+    if (!status) {
+	psError(PSASTRO_ERR_CONFIG, false, "Failed to build FPA from PSASTRO.INPUT");
+	return false;
+    }
+
+    // define the additional input/output files associated with psphot 
+    if (!psastroDefineFiles (config, input)) {
+	psError(PSASTRO_ERR_CONFIG, false, "Trouble defining the additional input/output files");
+	return false;
+    }
+
+    // Chip selection: turn on only the chips specified (option is not required)
+    char *chipLine = psMetadataLookupStr(&status, config->arguments, "CHIP_SELECTIONS"); 
+    psArray *chips = psStringSplitArray (chipLine, ",", false);
+    if (chips->n > 0) {
+	pmFPASelectChip (input->fpa, -1, true); // deselect all chips
+	for (int i = 0; i < chips->n; i++) {
+	    int chipNum = atoi(chips->data[i]);
+	    if (! pmFPASelectChip(input->fpa, chipNum, false)) {
+		psError(PSASTRO_ERR_CONFIG, true, "Chip number %d doesn't exist in camera.\n", chipNum);
+		return false;
+	    }
+        }
+    }
+    psFree (chips);
+
+    psString dump_file = psMetadataLookupStr(&status, config->arguments, "DUMP_CONFIG");
+    if (dump_file) {
+        pmConfigCamerasCull(config, NULL);
+        pmConfigRecipesCull(config, "PPIMAGE,PPSTATS,PSPHOT,MASKS,PSASTRO");
+
+        pmConfigDump(config, input->fpa, dump_file);
+    }
+
+
+    psTrace("psastro", 1, "Done with psastroParseCamera...\n");
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRefstarSubset.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRefstarSubset.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRefstarSubset.c	(revision 22306)
@@ -0,0 +1,90 @@
+# include "psastroInternal.h"
+
+bool psastroRefstarSubset (pmReadout *readout) {
+
+  // select the raw objects for this readout
+  psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+  if (rawstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing rawstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // select the raw objects for this readout
+  psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+  if (refstars == NULL)  {
+    psError(PSASTRO_ERR_DATA, false, "missing refstars in psastroRefstarSubset\n");
+    return false;
+  }
+
+  // calculate luminosity functions for rawstars and refstars
+  // the samples cover the same area (the chip), so no area correction
+  // is needed...
+  psLogMsg ("psastro", 4, "measuring luminosity function for rawstars\n");
+  pmLumFunc *rawfunc = psastroLuminosityFunction (rawstars);
+  if (rawfunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on rawstars for this readout\n");
+    return true;
+  }
+  psLogMsg ("psastro", 4, "measuring luminosity function for refstars\n");
+  pmLumFunc *reffunc = psastroLuminosityFunction (refstars);
+  if (reffunc == NULL) {
+    psLogMsg ("psastro", 4, "giving up on refstars for this readout\n");
+    return true;
+  }
+
+// XXX code to better deal with mismatches in the fitted lum function
+# if (0)
+  // if the fitted slopes differ by too much, give up and just try to match the peak bin
+  fSlope = (reffunc->slope / rawfunc->slope);
+  if ((fSlope > 1.3) || (fSlope < 0.77)) {
+      // XXX do something here (choose the peak of the smaller set, generate a histogram for
+      // the other set, then choose the bin from the larger set which has nBin = nPeak
+  }
+# endif
+
+  // what is the offset between the two lines at the average magnitude?
+  double mRef = 0.5*(reffunc->mMin + reffunc->mMax);
+  double logRho = mRef * reffunc->slope + reffunc->offset;
+  double mRaw = (logRho - rawfunc->offset) / rawfunc->slope;
+
+  psLogMsg ("psastro", 4, "mRef: %f, logRho: %f, mRaw: %f\n", mRef, logRho, mRaw);
+
+  double mRefMax = rawfunc->mMax - mRaw + mRef;
+  psLogMsg ("psastro", 4, "clipping stars fainter than %f\n", mRefMax);
+
+  psArray *subset = psArrayAllocEmpty (100);
+  for (int i = 0; i < refstars->n; i++) {
+    pmAstromObj *ref = refstars->data[i];
+    if (ref->Mag > mRefMax) continue;
+    psArrayAdd (subset, 100, ref);
+  }
+
+  psLogMsg ("psastro", 4, "keeping %ld of %ld reference stars\n", subset->n, refstars->n);
+
+  psMetadataRemoveKey (readout->analysis, "PSASTRO.REFSTARS");
+  psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSASTRO.REFSTARS", PS_DATA_ARRAY, "astrometry matches", subset);
+
+  if (psTraceGetLevel("psastro.dump") > 0) {
+      pmChip *chip = readout->parent->parent;
+
+      char *filename = NULL;
+      char *chipname = psMetadataLookupStr (NULL, chip->concepts, "CHIP.NAME");
+      psStringAppend (&filename, "refstars.%s.dat", chipname);
+      psastroDumpRefstars (subset, filename);
+      psFree (filename);
+  }
+
+  psFree (rawfunc);
+  psFree (reffunc);
+  psFree (subset);
+
+  return true;
+}
+
+/* this test is a bit sensitive to the total number of refstars or rawstars available
+   watch out if:
+   - the fitted slopes are extremely different 
+   - the average number of stars per bin is ~1
+   
+   skip the cut in these cases?
+*/
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRemoveClumps.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRemoveClumps.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroRemoveClumps.c	(revision 22306)
@@ -0,0 +1,97 @@
+# include "psastroInternal.h"
+
+// look for and exclude objects in clumps in the input list
+psArray *psastroRemoveClumps (psArray *input, int scale) {
+
+    // determine data range
+    pmAstromObj *obj = input->data[0];
+    float Xmin = obj->FP->x;
+    float Xmax = obj->FP->x;
+    float Ymin = obj->FP->y;
+    float Ymax = obj->FP->y;
+    for (int i = 0; i < input->n; i++) {
+	obj = (pmAstromObj *)input->data[i];
+	if (!isfinite(obj->FP->x)) continue;
+	if (!isfinite(obj->FP->y)) continue;
+	Xmin = PS_MIN (Xmin, obj->FP->x);
+	Xmax = PS_MAX (Xmax, obj->FP->x);
+	Ymin = PS_MIN (Ymin, obj->FP->y);
+	Ymax = PS_MAX (Ymax, obj->FP->y);
+    }
+
+    int nX = (Xmax - Xmin) / scale + 10;
+    int nY = (Ymax - Ymin) / scale + 10;
+    psImage *count = psImageAlloc (nX, nY, PS_TYPE_U32);
+    psImageInit (count, 0);
+
+    // accumulate 2D histogram in image
+    for (int i = 0; i < input->n; i++) {
+	obj = (pmAstromObj *)input->data[i];
+	if (!isfinite(obj->FP->x)) continue;
+	if (!isfinite(obj->FP->y)) continue;
+	int Xi = PS_MIN (PS_MAX((obj->FP->x - Xmin) / scale + 5, 0), count->numCols);
+	int Yi = PS_MIN (PS_MAX((obj->FP->y - Ymin) / scale + 5, 0), count->numRows);
+	count->data.U32[Yi][Xi] ++;
+    }
+
+    // determine image statistics
+    psStats *stats = psStatsAlloc (PS_STAT_MAX | PS_STAT_MAX | PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    if (!psImageStats(stats, count, NULL, 0)) {
+	psError(PS_ERR_UNKNOWN, false, "Unable to get image statistics.\n");
+	psFree(stats);
+	psFree(count);
+	return NULL;
+    }
+
+    if (stats->max < 1) {
+	psError(PS_ERR_UNKNOWN, false, "no valid sources in image\n");
+	psFree(stats);
+	psFree(count);
+	return NULL;
+    }
+
+    // XXX make this a user option
+    float limit = PS_MAX (5.0*stats->sampleStdev, 5.0);
+    psTrace ("psastro", 4, "skipping stars in cells with more than %f stars\n", limit);
+
+    // find and exclude objects in bad pixels
+    psArray *output = psArrayAllocEmpty (input->n);
+    for (int i = 0; i < input->n; i++) {
+	obj = (pmAstromObj *)input->data[i];
+	if (!isfinite(obj->FP->x)) continue;
+	if (!isfinite(obj->FP->y)) continue;
+	int Xi = PS_MIN (PS_MAX((obj->FP->x - Xmin) / scale + 5, 0), count->numCols);
+	int Yi = PS_MIN (PS_MAX((obj->FP->y - Ymin) / scale + 5, 0), count->numRows);
+	if (count->data.U32[Yi][Xi] > limit) continue;
+	psArrayAdd (output, 16, obj);
+    }
+
+    psFree(stats);
+    psFree(count);
+    return output;
+}
+
+# if (0)
+// make a list of the outlier pixels
+psArray *badpix = psArrayAllocEmpty (16);
+for (int iy = 0; iy < count->numRows; iy++) {
+    for (int ix = 0; ix < count->numCols; ix++) {
+	if (count->data.U32[iy][ix] > limit) {
+	    psPlane *pixel = psPlaneAlloc();
+	    pixel->x = ix;
+	    pixel->y = iy;
+	    psArrayAdd (badpix, 16, pixel);
+	    psFree (pixel);
+	}
+    }
+}
+
+if (badpix->n == 0) {
+    psArray *output = psMemIncrRefCounter (input);
+    psFree (stats);
+    psFree (count);
+    psFree (badpix);
+    return output;
+}
+# endif
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroStandAlone.h
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroStandAlone.h	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroStandAlone.h	(revision 22306)
@@ -0,0 +1,38 @@
+# ifdef HAVE_CONFIG_H
+# include <config.h>
+# endif
+
+#ifndef PSASTRO_STAND_ALONE_H
+#define PSASTRO_STAND_ALONE_H
+
+#include <stdio.h>
+#include <string.h>
+#include <pslib.h>
+#include <psmodules.h>
+#include "psastro.h"
+
+// Top level functions
+pmConfig         *psastroArguments (int argc, char **argv);
+void              psastroCleanup (pmConfig *config);
+bool              psastroParseCamera (pmConfig *config);
+bool              psastroDataLoad (pmConfig *config);
+
+pmConfig         *psastroModelArguments (int argc, char **argv);
+bool 		  psastroModelParseCamera (pmConfig *config);
+bool 		  psastroModelDataLoad (pmConfig *config);
+bool 		  psastroModelAnalysis (pmConfig *config);
+bool 		  psastroModelAdjust (pmConfig *config);
+bool 		  psastroModelDataSave (pmConfig *config);
+
+psVector         *psastroModelFitBoresite (psVector *Xo, psVector *Yo, psVector *Po, char *outroot);
+psF32 		  psastroModelBoresite (psVector *deriv, const psVector *params, const psVector *coord);
+
+// these are used to define the boresite model parameters
+# define PAR_X0  0  // Xo = params[0] 
+# define PAR_Y0  1  // Yo = params[1] 
+# define PAR_RX  2  // RX = params[2] 
+# define PAR_RY  3  // RY = params[3] 
+# define PAR_P0  4  // P0 = params[4]
+# define PAR_T0  5  // phi = params[4]
+
+#endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroTestFuncs.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroTestFuncs.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroTestFuncs.c	(revision 22306)
@@ -0,0 +1,45 @@
+# include "psastroInternal.h"
+
+// write out objects
+bool psastroWriteStars (char *filename, psArray *sources) {
+
+    // re-open, add data to end of file
+    FILE *f = fopen (filename, "w");
+    if (f == NULL) {
+	psLogMsg ("psastroWriteStars", 3, "can't open output file for output %s\n", filename);
+	return false;
+    }
+
+    for (int i = 0; i < sources->n; i++) {
+	
+	pmAstromObj *star = sources->data[i];
+
+	fprintf (f, "%8.2f %8.2f   %8.2f %8.2f   %8.2f %8.2f   %10.6f %10.6f   %8.2f %8.2f\n", 
+		 star->chip->x, star->chip->y, 
+		 star->FP->x, star->FP->y, 
+		 star->TP->x, star->TP->y, 
+		 star->sky->r*DEG_RAD, star->sky->d*DEG_RAD, 
+		 star->Mag, star->dMag);
+    }
+    fclose (f);
+    return true;
+}
+
+bool psastroWriteTransform (psPlaneTransform *map) {
+
+    // dump initial values:
+    for (int i = 0; i < map->x->nX + 1; i++) {
+	for (int j = 0; j < map->x->nY + 1; j++) {
+	    if (map->x->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "x term %d,%d: %f +/- %f\n", i, j, map->x->coeff[i][j], map->x->coeffErr[i][j]);
+	}
+    }
+
+    for (int i = 0; i < map->y->nX + 1; i++) {
+	for (int j = 0; j < map->y->nY + 1; j++) {
+	    if (map->y->coeffMask[i][j] & PS_POLY_MASK_SET) continue;
+	    psLogMsg ("psastro", 4, "y term %d,%d: %f +/- %f\n", i, j, map->y->coeff[i][j], map->y->coeffErr[i][j]);
+	}
+    }
+    return true;
+}
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUseModel.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUseModel.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUseModel.c	(revision 22306)
@@ -0,0 +1,138 @@
+# include "psastroInternal.h"
+# define NONLIN_TOL 0.001 /* tolerance in pixels */
+# define DEBUG 0
+
+// apply the generic astrometry model to this image.  this assumes the WCS terms either do not
+// exist or are invalid 
+bool psastroUseModel (pmConfig *config, psMetadata *recipe) {
+
+  bool status;
+
+  bool useModel = psMetadataLookupBool (&status, config->arguments, "PSASTRO.USE.MODEL");
+  if (!status) {
+      useModel = psMetadataLookupBool (&status, recipe, "PSASTRO.USE.MODEL");
+  }
+  if (!useModel) return true;
+
+  // identify reference astrometry table.  
+  // if not defined, correction was not requested; skip step
+  pmFPAfile *astrom = psMetadataLookupPtr (NULL, config->files, "PSASTRO.MODEL");
+  if (!astrom) psAbort ("programming error: model was not supplied, though requested");
+
+  // select the input data sources
+  pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+  if (!input) psAbort ("programming error: no input data");
+
+  // make sure the astrometry model is loaded.  de-activate all files except PSASTRO.MODEL.
+  pmFPAfileActivate (config->files, false, NULL);
+  pmFPAfileActivate (config->files, true, "PSASTRO.MODEL");
+
+  pmFPAview *view = pmFPAviewAlloc (0);
+
+  // files associated with the science image
+  if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) {
+      psError (PS_ERR_IO, false, "Can't load the astrometry model file");
+      return false;
+  }
+
+  // XXX TEST: apply the input image RA & DEC to the astrometry model, save corners
+  if (0) {
+      // these externally supplied values are used to set the final transformation terms
+      double RA  = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.RA");
+      double DEC = psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.DEC");
+      // double POS = PM_RAD_DEG * psMetadataLookupF64 (&status, input->fpa->concepts, "FPA.POSANGLE");
+
+      // get projection scale; center is supplied
+      // XXX should this be astrom or input??
+      float Xs = psMetadataLookupF32(&status, astrom->fpa->concepts, "XSCALE") * PM_RAD_DEG;
+      float Ys = psMetadataLookupF32(&status, astrom->fpa->concepts, "YSCALE") * PM_RAD_DEG;
+
+      // allocate a new toSky projection using the reported position
+      psFree (astrom->fpa->toSky);
+      astrom->fpa->toSky = psProjectionAlloc (RA, DEC, Xs, Ys, PS_PROJ_DIS);
+
+      // local view
+      pmFPAview *myView = pmFPAviewAlloc (0);
+
+      // loop over all chips, replace input astrometry elements with those from astrom 
+      pmChip *obsChip = NULL;
+      while ((obsChip = pmFPAviewNextChip (myView, input->fpa, 1)) != NULL) {
+	  psTrace ("psastro", 4, "Chip %d: %x %x\n", myView->chip, obsChip->file_exists, obsChip->process);
+	  if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+	  // set the chip astrometry using the astrom file
+	  pmChip *refChip = pmFPAviewThisChip (myView, astrom->fpa);
+
+	  psFree (obsChip->toFPA);
+	  psFree (obsChip->fromFPA);
+
+	  // supply astrometry from model 
+	  obsChip->toFPA   = psMemIncrRefCounter (refChip->toFPA);
+	  obsChip->fromFPA = psMemIncrRefCounter (refChip->fromFPA);
+
+	  // XXX if we want to write out the result, update the header here.  this needs to be
+	  // updated with the correct HDU selection.  obsChip->hdu may not exist.
+	  // pmAstromWriteBilevelChip (obsChip->hdu->header, obsChip, NONLIN_TOL);
+      }
+
+      psFree (input->fpa->toSky);
+      psFree (input->fpa->toTPA);
+      psFree (input->fpa->fromTPA);
+      input->fpa->toSky   = psMemIncrRefCounter (astrom->fpa->toSky);
+      input->fpa->toTPA   = psMemIncrRefCounter (astrom->fpa->toTPA);
+      input->fpa->fromTPA = psMemIncrRefCounter (astrom->fpa->fromTPA);
+
+      // XXX this is temporarily hardwired because of model error
+      input->fpa->toSky->type = PS_PROJ_TAN;
+
+      if (DEBUG) psastroDumpCorners ("corners.up.ast1.dat", "corners.dn.ast1.dat", input->fpa);
+  }
+
+  // set the model using the RA, DEC, POSANGLE of the input image.
+  pmAstromModelSetTP (astrom, input->fpa->concepts);
+
+  if (DEBUG) psastroDumpCorners ("corners.up.ast2.dat", "corners.dn.ast2.dat", astrom->fpa);
+
+  // loop over all chips, replace input astrometry elements with those from astrom 
+  pmChip *obsChip = NULL;
+  while ((obsChip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+    psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, obsChip->file_exists, obsChip->process);
+    if (!obsChip->process || !obsChip->file_exists || !obsChip->data_exists) { continue; }
+
+    // set the chip astrometry using the astrom file
+    pmChip *refChip = pmFPAviewThisChip (view, astrom->fpa);
+
+    psFree (obsChip->toFPA);
+    psFree (obsChip->fromFPA);
+
+    // supply astrometry from model 
+    obsChip->toFPA   = psMemIncrRefCounter (refChip->toFPA);
+    obsChip->fromFPA = psMemIncrRefCounter (refChip->fromFPA);
+
+    // XXX if we want to write out the result, update the header here.  this needs to be
+    // updated with the correct HDU selection.  obsChip->hdu may not exist.
+    // pmAstromWriteBilevelChip (obsChip->hdu->header, obsChip, NONLIN_TOL);
+  }
+
+  psFree (input->fpa->toSky);
+  psFree (input->fpa->toTPA);
+  psFree (input->fpa->fromTPA);
+  input->fpa->toSky   = psMemIncrRefCounter (astrom->fpa->toSky);
+  input->fpa->toTPA   = psMemIncrRefCounter (astrom->fpa->toTPA);
+  input->fpa->fromTPA = psMemIncrRefCounter (astrom->fpa->fromTPA);
+
+  // XXX this is temporarily hardwired because of model error
+  input->fpa->toSky->type = PS_PROJ_TAN;
+
+  if (DEBUG) psastroDumpCorners ("corners.up.inp.dat", "corners.dn.inp.dat", input->fpa);
+
+  // updated with the correct HDU selection.  obsChip->hdu may not exist.
+  // psMetadata *updates = psMetadataAlloc();
+  // pmAstromWriteBilevelMosaic (updates, input->fpa, NONLIN_TOL);
+  // psMetadataAddMetadata (input->fpa->analysis, PS_LIST_TAIL, "PSASTRO.HEADER",  PS_META_REPLACE, "psastro header stats", updates);
+  // psFree (updates);
+
+  psFree (view);
+  return true;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUtils.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUtils.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroUtils.c	(revision 22306)
@@ -0,0 +1,217 @@
+# include "psastroInternal.h"
+# define RENORM 0
+
+// fix this to look up the value in the chip concepts
+static double getChipPixelScale (pmChip *chip) {
+    return 10.0;
+}
+
+// I have an FPA structure with multiple chips.  we have loaded or measured astrometry for each
+// chip with independent pixel/degree scaling values.  These will naturally compensate locally
+// somewhat for the telescope distortion.  to measure the telescope distortion, we need to
+// force the chips to have the same pixel scale and measure the difference from that solution.
+// Convert an FPA with disparate pixel scales to a common pixel scale (perhaps depending on the
+// chip -- eg, TC3)
+
+bool psastroMosaicCommonScale (pmFPA *fpa, psMetadata *recipe) {
+
+    // options : use the MIN or MAX chip as global reference or supplied pixel scales
+    // (microns/pixel), which may depend on the chip
+
+    float pixelScaleUse = 1.0, pixelScale1 = 1.0,  pixelScale2 = 1.0,  pixelScale = 1.0;
+
+    char *option = psMetadataLookupStr (NULL, recipe, "PSASTRO.COMMON.SCALE.OPTION");
+    if (option == NULL) {
+	psError(PSASTRO_ERR_DATA, false, "no choice set for common scale option\n");
+	return false;
+    }
+
+    bool useExternal = true;
+
+    // find the min or max scale chip
+    if (!strcasecmp (option, "MIN") || !strcasecmp (option, "MAX")) {
+
+	bool useMax = !strcasecmp (option, "MAX");
+	pixelScaleUse = (useMax) ? FLT_MIN : FLT_MAX;
+
+	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; }
+
+	    pixelScale1 = hypot (chip->toFPA->x->coeff[1][0], chip->toFPA->x->coeff[0][1]);
+	    pixelScale2 = hypot (chip->toFPA->y->coeff[1][0], chip->toFPA->y->coeff[0][1]);
+	    pixelScale = 0.5*(pixelScale1 + pixelScale2);
+	    
+	    pixelScaleUse = (useMax) ? PS_MAX (pixelScale, pixelScaleUse) : PS_MIN (pixelScale, pixelScaleUse);
+	}
+	useExternal = false;
+    }
+
+    // rescale each chip by the reference scale
+    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; }
+
+	psPlaneTransform *toFPA = chip->toFPA;
+	psPlaneTransform *fromFPA = chip->fromFPA;
+	    
+	pixelScale1 = hypot (toFPA->x->coeff[1][0], toFPA->x->coeff[0][1]);
+	pixelScale2 = hypot (toFPA->y->coeff[1][0], toFPA->y->coeff[0][1]);
+
+	if (useExternal) {
+	    pixelScaleUse = getChipPixelScale (chip);
+	}
+
+	for (int i = 0; i <= toFPA->x->nX; i++) {
+	    for (int j = 0; j <= toFPA->x->nX; j++) {
+		toFPA->x->coeff[i][j] *= pixelScaleUse/pixelScale1;
+		toFPA->y->coeff[i][j] *= pixelScaleUse/pixelScale2;
+		fromFPA->x->coeff[i][j] *= pixelScale1/pixelScaleUse;
+		fromFPA->y->coeff[i][j] *= pixelScale2/pixelScaleUse;
+	    }
+	}
+
+    }
+    psastroMosaicSetAstrom (fpa);
+    return true;
+}
+
+bool psastroUpdateChipToFPA (pmFPA *fpa, pmChip *chip, psArray *rawstars, psArray *refstars) {
+
+    psRegion *region = pmChipPixels (chip);
+
+    psFree (chip->fromFPA);
+    chip->fromFPA = psPlaneTransformInvert (NULL, chip->toFPA, *region, 50);
+    psFree (region);
+
+    for (int i = 0; i < rawstars->n; i++) {
+        pmAstromObj *raw = rawstars->data[i];
+
+        psPlaneTransformApply (raw->FP, chip->toFPA, raw->chip);
+        psPlaneTransformApply (raw->TP, fpa->toTPA, raw->FP);
+        psDeproject (raw->sky, raw->TP, fpa->toSky);
+    }
+
+    for (int i = 0; i < refstars->n; i++) {
+        pmAstromObj *ref = refstars->data[i];
+        psPlaneTransformApply (ref->chip, chip->fromFPA, ref->FP);
+    }
+
+    return true;
+}
+
+# if 0
+
+bool psastroSelectBrightStars (pmFPA *fpa, psMetadata *config) {
+
+    bool status;
+
+    // add exclusions for objects on some basis?
+    int MAX_NSTARS = psMetadataLookupS32 (&status, config, "PSASTRO.STARS.MAX");
+
+    for (int i = 0; i < fpa->chips->n; i++) {
+        pmChip *chip = fpa->chips->data[i];
+        for (int j = 0; j < chip->cells->n; j++) {
+            pmCell *cell = chip->cells->data[j];
+            for (int k = 0; k < cell->readouts->n; k++) {
+                pmReadout *readout = cell->readouts->data[k];
+
+                psArray *stars = psMetadataLookupPtr (&status, readout->analysis, "STARS.FULLSET");
+                stars = psArraySort (stars, pmAstromObjSortByMag);
+
+                int nSubset = PS_MIN (MAX_NSTARS, stars->n);
+                psArray *subset = psArrayAlloc (nSubset);
+
+                for (int i = 0; i < nSubset; i++) {
+                    subset->data[i] = stars->data[i];
+                }
+                psMetadataAdd (readout->analysis, PS_LIST_TAIL, "STARS.SUBSET", PS_DATA_ARRAY, "stars from analysis", subset);
+            }
+        }
+    }
+    return true;
+}
+
+psPlaneDistort *psPlaneDistortCopy (psPlaneDistort *input) {
+
+    psPlaneDistort *output = psPlaneDistortAlloc (input->x->nX, input->x->nY, input->x->nZ, input->x->nT);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            for (int k = 0; k < input->x->nZ; k++) {
+                for (int m = 0; m < input->x->nT; m++) {
+                    // x-terms
+                    output->x->mask[i][j][k][m]     = input->x->mask[i][j][k][m];
+                    output->x->coeff[i][j][k][m]    = input->x->coeff[i][j][k][m];
+                    output->x->coeffErr[i][j][k][m] = input->x->coeffErr[i][j][k][m];
+                    // y-terms
+                    output->y->mask[i][j][k][m]     = input->y->mask[i][j][k][m];
+                    output->y->coeff[i][j][k][m]    = input->y->coeff[i][j][k][m];
+                    output->y->coeffErr[i][j][k][m] = input->y->coeffErr[i][j][k][m];
+                }
+            }
+        }
+    }
+    return (output);
+}
+
+psPlaneTransform *psPlaneTransformCopy (psPlaneTransform *input) {
+
+    psPlaneTransform *output = psPlaneTransformAlloc (input->x->nX, input->x->nY);
+
+    for (int i = 0; i < input->x->nX; i++) {
+        for (int j = 0; j < input->x->nY; j++) {
+            // x-terms
+            output->x->mask[i][j]     = input->x->mask[i][j];
+            output->x->coeff[i][j]    = input->x->coeff[i][j];
+            output->x->coeffErr[i][j] = input->x->coeffErr[i][j];
+            // y-terms
+            output->y->mask[i][j]     = input->y->mask[i][j];
+            output->y->coeff[i][j]    = input->y->coeff[i][j];
+            output->y->coeffErr[i][j] = input->y->coeffErr[i][j];
+        }
+    }
+    return (output);
+}
+
+psProjection *psProjectionCopy (psProjection *input) {
+
+    psProjection *output = psProjectionAlloc (input->R, input->D, input->Xs, input->Ys, input->type);
+    return (output);
+}
+
+// returns the rotation term, forcing positive parity
+double psPlaneTransformGetRotation (psPlaneTransform *map) {
+
+    if (map->x->nX < 1) return 0;
+    if (map->x->nY < 1) return 0;
+
+    if (map->y->nX < 1) return 0;
+    if (map->y->nY < 1) return 0;
+
+    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];
+
+    double px = SIGN (pc1_1);
+    double py = SIGN (pc2_2);
+
+    // both x and y terms imply an angle. take the average
+    double t1 = -atan2 (px*pc1_2, px*pc1_1);
+    double t2 = +atan2 (py*pc2_1, py*pc2_2);
+
+    // careful near -pi,+pi boundary...
+    if (t1 - t2 > M_PI/2) t2 += 2*M_PI;
+    if (t2 - t1 > M_PI/2) t1 += 2*M_PI;
+
+    double theta = 0.5*(t1 + t2);
+    while (theta < M_PI) theta += 2*M_PI;
+    while (theta > M_PI) theta -= 2*M_PI;
+
+    return (theta);
+}
+
+# endif
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroVersion.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroVersion.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroVersion.c	(revision 22306)
@@ -0,0 +1,20 @@
+#include "psastroInternal.h"
+
+static const char *cvsTag = "$Name: not supported by cvs2svn $";// CVS tag name
+
+psString psastroVersion(void)
+{
+    psString version = NULL;            // Version, to return
+    psStringAppend(&version, "%s-%s",PACKAGE_NAME,PACKAGE_VERSION);
+    return version;
+}
+
+psString psastroVersionLong(void)
+{
+    psString version = psastroVersion(); // Version, to return
+    psString tag = psStringStripCVS(cvsTag, "Name"); // CVS tag
+    psStringAppend(&version, " (cvs tag %s) %s, %s", tag, __DATE__, __TIME__);
+    psFree(tag);
+    return version;
+}
+
Index: /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroZeroPoint.c
===================================================================
--- /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroZeroPoint.c	(revision 22306)
+++ /tags/cnb_tags/cnb_branch_oct9_08/psastro/src/psastroZeroPoint.c	(revision 22306)
@@ -0,0 +1,108 @@
+# include "psastroInternal.h"
+
+bool psastroZeroPointReadout(psArray *rawstars, psArray *refstars, psArray *matches);
+
+bool psastroZeroPoint (pmConfig *config) {
+
+    pmChip *chip = NULL;
+    pmCell *cell = NULL;
+    pmReadout *readout = NULL;
+
+    // select the current recipe
+    psMetadata *recipe  = psMetadataLookupPtr (NULL, config->recipes, PSASTRO_RECIPE);
+    if (!recipe) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find PSASTRO recipe!\n");
+	return false;
+    }
+
+    // select the input data sources
+    pmFPAfile *input = psMetadataLookupPtr (NULL, config->files, "PSASTRO.INPUT");
+    if (!input) {
+	psError(PSASTRO_ERR_CONFIG, false, "Can't find input data!\n");
+	return false;
+    }
+    pmFPA *fpa = input->fpa;
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // given the existing per-chip astrometry, determine matches between raw and ref stars
+    // is this needed? yes, if we didn't do SingleChip astrometry first
+    if (!psastroMosaicSetMatch (fpa, recipe, 0)) {
+	psError(PSASTRO_ERR_UNKNOWN, false, "failed to match raw and ref stars for mosaic");
+	return false;
+    }
+
+    // this loop selects the matched stars for all chips
+    while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+        psTrace ("psastro", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (!chip->process || !chip->file_exists) { continue; }
+	if (!chip->toFPA) { continue; }
+
+	while ((cell = pmFPAviewNextCell (view, fpa, 1)) != NULL) {
+            psTrace ("psastro", 4, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+            if (!cell->process || !cell->file_exists) { continue; }
+
+	    // process each of the readouts
+	    // XXX there can only be one readout per chip, right?
+	    while ((readout = pmFPAviewNextReadout (view, fpa, 1)) != NULL) {
+		if (! readout->data_exists) { continue; }
+
+		// select the raw objects for this readout
+		psArray *rawstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.RAWSTARS");
+		if (rawstars == NULL) { continue; }
+
+		// select the raw objects for this readout
+		psArray *refstars = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.REFSTARS");
+		if (refstars == NULL) { continue; }
+
+		psArray *matches = psMetadataLookupPtr (NULL, readout->analysis, "PSASTRO.MATCH");
+		if (matches == NULL) { continue; }
+
+		// calculate dMag for the matched stars
+		psastroZeroPointReadout (rawstars, refstars, matches);
+	    }
+	}
+    }
+
+    psFree (view);
+    return true;
+}
+
+// XXX for now, let's just measure <dMag> and sigma_dMag
+// XXX a test comment
+bool psastroZeroPointReadout(psArray *rawstars, psArray *refstars, psArray *matches) {
+
+    psVector *dMag  = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    int Npts = 0;
+    for (int i = 0; i < matches->n; i++) {
+
+      pmAstromMatch *match = matches->data[i];
+
+      pmAstromObj *raw = rawstars->data[match->raw];
+
+      pmAstromObj *ref = refstars->data[match->ref];
+
+      dMag->data.F32[Npts] = ref->Mag - raw->Mag;
+      psVectorExtend (dMag, 100, 1);
+      Npts++;
+    }
+
+    psTrace ("psModules.astrom", 4, "Npts: %d\n", Npts);
+
+    if (Npts < 3) {
+      fprintf (stderr, "zero point NaN +/- NaN\n");
+      psFree (dMag);
+      return false;
+    }
+
+    // stats structure and mask for use in measuring the clipping statistic
+    // this analysis has too few data points to use the robust median method
+    psStats *stats = psStatsAlloc (PS_STAT_CLIPPED_MEAN | PS_STAT_CLIPPED_STDEV);
+    psVectorStats (stats, dMag, NULL, NULL, 0);
+    fprintf (stderr, "zero point %f +/- %f\n", stats->clippedMean, stats->clippedStdev);
+
+    psFree (dMag);
+    psFree (stats);
+    return true;
+}
