/** @file  pmPSFtry.c
 *
 *  XXX: need description of file purpose
 *
 *  @author EAM, IfA
 *
 *  @version $Revision: 1.61 $ $Name: not supported by cvs2svn $
 *  @date $Date: 2008-09-11 00:38:23 $
 *
 *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

# include <pslib.h>
#include "pmHDU.h"
#include "pmFPA.h"
#include "pmFPAMaskWeight.h"
#include "pmPeaks.h"
#include "pmMoments.h"
#include "pmResiduals.h"
#include "pmGrowthCurve.h"
#include "pmTrend2D.h"
#include "pmPSF.h"
#include "pmModel.h"
#include "pmSource.h"
#include "pmSourceUtils.h"
#include "pmPSFtry.h"
#include "pmModelClass.h"
#include "pmModelUtils.h"
#include "pmSourceFitModel.h"
#include "pmSourcePhotometry.h"

// ********  pmPSFtry functions  **************************************************
// * pmPSFtry holds a single pmPSF model test, with the input sources, the freely
// * fitted version of the model, the pmPSF fit to the fitted model parameters,
// * and the PSF fits to the source. It also includes the statistics from the
// * fits, both the individual sources, and the collection

// free a pmPSFtry structure
static void pmPSFtryFree (pmPSFtry *test)
{
    if (test == NULL) return;

    psFree (test->psf);
    psFree (test->sources);
    psFree (test->metric);
    psFree (test->metricErr);
    psFree (test->fitMag);
    psFree (test->mask);
    return;
}

// allocate a pmPSFtry based on the desired sources and the model (identified by name)
pmPSFtry *pmPSFtryAlloc (const psArray *sources, const pmPSFOptions *options)
{
    pmPSFtry *test = (pmPSFtry *) psAlloc(sizeof(pmPSFtry));
    psMemSetDeallocator(test, (psFreeFunc) pmPSFtryFree);

    test->psf       = pmPSFAlloc (options);
    test->metric    = psVectorAlloc (sources->n, PS_TYPE_F32);
    test->metricErr = psVectorAlloc (sources->n, PS_TYPE_F32);
    test->fitMag    = psVectorAlloc (sources->n, PS_TYPE_F32);
    test->mask      = psVectorAlloc (sources->n, PS_TYPE_U8);

    psVectorInit (test->mask,        0);
    psVectorInit (test->metric,    0.0);
    psVectorInit (test->metricErr, 0.0);
    psVectorInit (test->fitMag,    0.0);

    test->sources   = psArrayAlloc (sources->n);

    for (int i = 0; i < sources->n; i++) {
        test->sources->data[i] = pmSourceCopy (sources->data[i]);
    }

    return (test);
}

bool psMemCheckPSFtry(psPtr ptr)
{
    PS_ASSERT_PTR(ptr, false);
    return ( psMemGetDeallocator(ptr) == (psFreeFunc) pmPSFtryFree);
}


// build a pmPSFtry for the given model:
// - fit each source with the free-floating model
// - construct the pmPSF from the collection of models
// - fit each source with the PSF-parameter models
// - measure the pmPSF quality metric (dApResid)

// sources used in for pmPSFtry may be masked by the analysis
// mask values indicate the reason the source was rejected:

// generate a pmPSFtry with a copy of the test PSF sources
pmPSFtry *pmPSFtryModel (const psArray *sources, const char *modelName, pmPSFOptions *options, psMaskType maskVal, psMaskType markVal)
{
    bool status;
    int Next = 0;
    int Npsf = 0;

    // validate the requested model name
    options->type = pmModelClassGetType (modelName);
    if (options->type == -1) {
        psError (PS_ERR_UNKNOWN, true, "invalid model name %s", modelName);
        return NULL;
    }

    pmPSFtry *psfTry = pmPSFtryAlloc (sources, options);
    if (psfTry == NULL) {
        psError (PS_ERR_UNKNOWN, false, "failed to allocate psf model");
        return NULL;
    }

    // maskVal is used to test for rejected pixels, and must include markVal
    maskVal |= markVal;

    // stage 1:  fit an EXT model to all candidates PSF sources
    psTimerStart ("fit");
    for (int i = 0; i < psfTry->sources->n; i++) {

        pmSource *source = psfTry->sources->data[i];
        source->modelEXT = pmSourceModelGuess (source, psfTry->psf->type);
        if (source->modelEXT == NULL) {
            psError(PS_ERR_UNKNOWN, false, "failed to build model");
            psFree(psfTry);
            return NULL;
        }

        // set object mask to define valid pixels
        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "OR", markVal);

        // fit model as EXT, not PSF
        status = pmSourceFitModel (source, source->modelEXT, PM_SOURCE_FIT_EXT, maskVal);

        // clear object mask to define valid pixels
        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "AND", PS_NOT_U8(markVal));

        // exclude the poor fits
        if (!status) {
            psfTry->mask->data.U8[i] = PSFTRY_MASK_EXT_FAIL;
            psTrace ("psModules.objects", 4, "masking %d (%d,%d) : status is poor\n", i, source->peak->x, source->peak->y);
            continue;
        }
        Next ++;
    }
    psLogMsg ("psphot.psftry", 4, "fit ext:   %f sec for %d of %ld sources\n", psTimerMark ("fit"), Next, sources->n);
    psTrace ("psphot.psftry", 3, "keeping %d of %ld PSF candidates (EXT)\n", Next, sources->n);

    if (Next == 0) {
        psError(PS_ERR_UNKNOWN, true, "No sources with good extended fits from which to determine PSF.");
        psFree(psfTry);
        return NULL;
    }

    // stage 2: construct a psf (pmPSF) from this collection of model fits
    if (!pmPSFFromPSFtry (psfTry)) {
        psError(PS_ERR_UNKNOWN, false, "failed to construct a psf model from collection of sources");
        psFree(psfTry);
        return NULL;
    }

    // stage 3: refit with fixed shape parameters
    psTimerStart ("fit");
    for (int i = 0; i < psfTry->sources->n; i++) {

        pmSource *source = psfTry->sources->data[i];

        // masked for: bad model fit, outlier in parameters
        if (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL) {
            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : source is masked\n", i, source->peak->x, source->peak->y);
            continue;
        }

        // set shape for this model based on PSF
        source->modelPSF = pmModelFromPSF (source->modelEXT, psfTry->psf);
        if (source->modelPSF == NULL) {
            psfTry->mask->data.U8[i] = PSFTRY_MASK_BAD_MODEL;
            abort();
            continue;
        }
        source->modelPSF->radiusFit = options->radius;

        // set object mask to define valid pixels
        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "OR", markVal);

        // fit the PSF model to the source
        status = pmSourceFitModel (source, source->modelPSF, PM_SOURCE_FIT_PSF, maskVal);

        // skip poor fits
        if (!status) {
            psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "AND", PS_NOT_U8(markVal));
            psfTry->mask->data.U8[i] = PSFTRY_MASK_PSF_FAIL;
            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : failed PSF fit\n", i, source->peak->x, source->peak->y);
            continue;
        }

        status = pmSourceMagnitudes (source, psfTry->psf, PM_SOURCE_PHOT_INTERP, maskVal);
        if (!status || isnan(source->apMag)) {
            psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "AND", PS_NOT_U8(markVal));
            psfTry->mask->data.U8[i] = PSFTRY_MASK_BAD_PHOT;
            psTrace ("psModules.objects", 4, "dropping %d (%d,%d) : poor photometry\n", i, source->peak->x, source->peak->y);
            continue;
        }

        // clear object mask to define valid pixels
        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, options->radius, "AND", PS_NOT_U8(markVal));

        psfTry->fitMag->data.F32[i] = source->psfMag;
        psfTry->metric->data.F32[i] = source->apMag - source->psfMag;
        psfTry->metricErr->data.F32[i] = source->errMag;

        psTrace ("psphot.psftry", 6, "keeping source %d (%d) of %ld\n", i, Npsf, psfTry->sources->n);
        Npsf ++;
    }
    psfTry->psf->nPSFstars = Npsf;

    psLogMsg ("psphot.psftry", 4, "fit psf:   %f sec for %d of %ld sources\n", psTimerMark ("fit"), Npsf, sources->n);
    psTrace ("psphot.psftry", 3, "keeping %d of %ld PSF candidates (PSF)\n", Npsf, sources->n);

    if (Npsf == 0) {
        psError(PS_ERR_UNKNOWN, true, "No sources with good PSF fits after model is built.");
        psFree(psfTry);
        return NULL;
    }

    // measure the chi-square trend as a function of flux (PAR[PM_PAR_I0])
    // this should be linear for Poisson errors and quadratic for constant sky errors
    psVector *flux  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
    psVector *chisq = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
    psVector *mask  = psVectorAlloc (psfTry->sources->n, PS_TYPE_MASK);

    // generate the x and y vectors, and mask missing models
    for (int i = 0; i < psfTry->sources->n; i++) {
        pmSource *source = psfTry->sources->data[i];
        if (source->modelPSF == NULL) {
            flux->data.F32[i] = 0.0;
            chisq->data.F32[i] = 0.0;
            mask->data.U8[i] = 0xff;
        } else {
            flux->data.F32[i] = source->modelPSF->params->data.F32[PM_PAR_I0];
            chisq->data.F32[i] = source->modelPSF->chisq / source->modelPSF->nDOF;
            mask->data.U8[i] = 0;
        }
    }

    // use 3hi/3lo sigma clipping on the chisq fit
    psStats *stats = options->stats;

    // linear clipped fit of chisq trend vs flux
    bool result = psVectorClipFitPolynomial1D(psfTry->psf->ChiTrend, options->stats, mask,
                                              0xff, chisq, NULL, flux);
    psStatsOptions meanStat = psStatsMeanOption(options->stats->options); // Statistic for mean
    psStatsOptions stdevStat = psStatsStdevOption(options->stats->options); // Statistic for stdev

    psLogMsg ("pmPSFtry", 4, "chisq vs flux fit: %f +/- %f\n",
              psStatsGetValue(stats, meanStat), psStatsGetValue(stats, stdevStat));

    psFree(flux);
    psFree(mask);
    psFree(chisq);

    if (!result) {
        psError(PS_ERR_UNKNOWN, false, "Failed to fit psf->ChiTrend");
        psFree(psfTry);
        return NULL;
    }

    for (int i = 0; i < psfTry->psf->ChiTrend->nX + 1; i++) {
        psLogMsg ("pmPSFtry", 4, "chisq vs flux fit term %d: %f +/- %f\n", i,
                  psfTry->psf->ChiTrend->coeff[i]*pow(10000, i),
                  psfTry->psf->ChiTrend->coeffErr[i]*pow(10000,i));
    }

    // XXX this function wants aperture radius for pmSourcePhotometry
    if (!pmPSFtryMetric (psfTry, options)) {
        psError(PS_ERR_UNKNOWN, false, "Attempt to fit PSF with model %s failed.", modelName);
        psFree (psfTry);
        return NULL;
    }

    psLogMsg ("psphot.pspsf", 3, "try model %s, ap-fit: %f +/- %f : sky bias: %f\n",
              modelName, psfTry->psf->ApResid, psfTry->psf->dApResid, psfTry->psf->skyBias);

    return (psfTry);
}

bool pmPSFtryMetric (pmPSFtry *psfTry, pmPSFOptions *options)
{
    PS_ASSERT_PTR_NON_NULL(psfTry, false);
    PS_ASSERT_PTR_NON_NULL(options, false);
    PS_ASSERT_PTR_NON_NULL(psfTry->sources, false);

    float RADIUS = options->radius;

    // the measured (aperture - fit) magnitudes (dA == psfTry->metric)
    //   depend on both the true ap-fit (dAo) and the bias in the sky measurement:
    //     dA = dAo + dsky/flux
    //   where flux is the flux of the star
    // we fit this trend to find the infinite flux aperture correction (dAo),
    //   the nominal sky bias (dsky), and the error on dAo
    // the values of dA are contaminated by stars with close neighbors in the aperture
    //   we use an outlier rejection to avoid this bias

    // r2rflux = radius^2 * ten(0.4*fitMag);
    psVector *r2rflux = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);

    for (int i = 0; i < psfTry->sources->n; i++) {
        if (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL)
            continue;
        r2rflux->data.F32[i] = PS_SQR(RADIUS) * pow(10.0, 0.4*psfTry->fitMag->data.F32[i]);
    }

    // XXX test dump of aperture residual data
    if (psTraceGetLevel("psModules.objects") >= 5) {
        FILE *f = fopen ("apresid.dat", "w");
        for (int i = 0; i < psfTry->sources->n; i++) {
            int keep = (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL);

            pmSource *source = psfTry->sources->data[i];
            float x = source->peak->x;
            float y = source->peak->y;

            fprintf (f, "%d  %d, %f %f %f  %f %f %f \n",
                     i, keep, x, y,
                     psfTry->fitMag->data.F32[i],
                     r2rflux->data.F32[i],
                     psfTry->metric->data.F32[i],
                     psfTry->metricErr->data.F32[i]);
        }
        fclose (f);
    }

    // This analysis of the apResid statistics is only approximate.  The fitted magnitudes
    // measured at this point (in the PSF fit) use Poisson errors, and are thus biased as a
    // function of magnitude.  We re-measure the apResid statistics later in psphot using the
    // linear, constant-error fitting.  Do not reject outliers with excessive vigor here.

    // fit ApTrend only to r2rflux, ignore x,y,flux variations for now
    // linear clipped fit of ApResid to r2rflux
    psPolynomial1D *poly = psPolynomial1DAlloc (PS_POLYNOMIAL_ORD, 1);
    poly->coeffMask[1] = PS_POLY_MASK_SET; // fit only a constant offset (no SKYBIAS)

    // XXX replace this with a psVectorStats call?  since we are not fitting the trend
    bool result = psVectorClipFitPolynomial1D(poly, options->stats, psfTry->mask, PSFTRY_MASK_ALL,
                                              psfTry->metric, psfTry->metricErr, r2rflux);
    if (!result) {
        psError(PS_ERR_UNKNOWN, false, "Failed to fit clipped poly");

        psFree(poly);
        psFree(r2rflux);

        return false;
    }
    psStatsOptions stdevStat = psStatsStdevOption(options->stats->options); // Statistic for stdev
    psLogMsg ("pmPSFtryMetric", 4, "apresid: %f +/- %f; from statistics of %ld psf stars\n", poly->coeff[0],
              psStatsGetValue(options->stats, stdevStat), psfTry->sources->n);



    // XXX test dump of fitted model (dump when tracing?)
    if (psTraceGetLevel("psModules.objects") >= 4) {
        FILE *f = fopen ("resid.dat", "w");
        psVector *apfit = psPolynomial1DEvalVector (poly, r2rflux);
        for (int i = 0; i < psfTry->sources->n; i++) {
            int keep = (psfTry->mask->data.U8[i] & PSFTRY_MASK_ALL);

            pmSource *source = psfTry->sources->data[i];
            float x = source->peak->x;
            float y = source->peak->y;

            fprintf (f, "%d  %d, %f %f %f  %f %f %f  %f\n",
                     i, keep, x, y,
                     psfTry->fitMag->data.F32[i],
                     r2rflux->data.F32[i],
                     psfTry->metric->data.F32[i],
                     psfTry->metricErr->data.F32[i],
                     apfit->data.F32[i]);
        }
        fclose (f);
        psFree (apfit);
    }

    // XXX drop the skyBias value, or include above??
    psfTry->psf->skyBias  = poly->coeff[1];
    psfTry->psf->ApResid  = poly->coeff[0];
    psfTry->psf->dApResid = psStatsGetValue(options->stats, stdevStat);

    psFree (r2rflux);
    psFree (poly);

    return true;
}

/*
  (aprMag' - fitMag) = rflux*skyBias + ApTrend(x,y)
  (aprMag - rflux*skyBias) - fitMag = ApTrend(x,y)
  (aprMag - rflux*skyBias) = fitMag + ApTrend(x,y)
*/

/*****************************************************************************
pmPSFFromPSFtry (psfTry): build a PSF model from a collection of
source->modelEXT entries.  The PSF ignores the first 4 (independent) model
parameters and constructs a polynomial fit to the remaining as a function of
image coordinate.
    input: psfTry with fitted source->modelEXT collection, pre-allocated psf
Note: some of the array entries may be NULL (failed fits); ignore them.
 *****************************************************************************/
bool pmPSFFromPSFtry (pmPSFtry *psfTry)
{
    PS_ASSERT_PTR_NON_NULL(psfTry, false);
    PS_ASSERT_PTR_NON_NULL(psfTry->sources, false);

    pmPSF *psf = psfTry->psf;

    // construct the fit vectors from the collection of objects
    psVector *x  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
    psVector *y  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);
    psVector *z  = psVectorAlloc (psfTry->sources->n, PS_TYPE_F32);

    // construct the x,y terms
    for (int i = 0; i < psfTry->sources->n; i++) {
        pmSource *source = psfTry->sources->data[i];
        if (source->modelEXT == NULL)
            continue;

        x->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_XPOS];
        y->data.F32[i] = source->modelEXT->params->data.F32[PM_PAR_YPOS];
    }

    if (!pmPSFFitShapeParams (psf, psfTry->sources, x, y, psfTry->mask)) {
        psFree(x);
        psFree(y);
        psFree(z);
        return false;
    }

    // skip the unfitted parameters (X, Y, Io, Sky) and the shape parameters (SXX, SYY, SXY)
    // apply the values of Nx, Ny determined above for E0,E1,E2 to the remaining parameters
    for (int i = 0; i < psf->params->n; i++) {
        switch (i) {
          case PM_PAR_SKY:
          case PM_PAR_I0:
          case PM_PAR_XPOS:
          case PM_PAR_YPOS:
          case PM_PAR_SXX:
          case PM_PAR_SYY:
          case PM_PAR_SXY:
            continue;
          default:
            break;
        }

        // select the per-object fitted data for this parameter
        for (int j = 0; j < psfTry->sources->n; j++) {
            pmSource *source = psfTry->sources->data[j];
            if (source->modelEXT == NULL) continue;
            z->data.F32[j] = source->modelEXT->params->data.F32[i];
        }

        psImageBinning *binning = psImageBinningAlloc();
        binning->nXruff = psf->trendNx;
        binning->nYruff = psf->trendNy;
        binning->nXfine = psf->fieldNx;
        binning->nYfine = psf->fieldNy;

        if (psf->psfTrendMode == PM_TREND_MAP) {
            psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
            psImageBinningSetSkipByOffset (binning, psf->fieldXo, psf->fieldYo);
        }

        // free existing trend, re-alloc
        psFree (psf->params->data[i]);
        psf->params->data[i] = pmTrend2DNoImageAlloc (psf->psfTrendMode, binning, psf->psfTrendStats);
        psFree (binning);

        // fit the collection of measured parameters to the PSF 2D model
        // the mask is carried from previous steps and updated with this operation
        // the weight is either the flux error or NULL, depending on 'psf->poissonErrorParams'
        if (!pmTrend2DFit (psf->params->data[i], psfTry->mask, 0xff, x, y, z, NULL)) {
            psError(PS_ERR_UNKNOWN, false, "failed to build psf model for parameter %d", i);
            psFree(x);
            psFree(y);
            psFree(z);
            return false;
        }
    }

    // test dump of star parameters vs position (compare with fitted values)
    if (psTraceGetLevel("psModules.objects") >= 4) {
        FILE *f = fopen ("params.dat", "w");

        for (int j = 0; j < psfTry->sources->n; j++) {
            pmSource *source = psfTry->sources->data[j];
            if (source == NULL) continue;
            if (source->modelEXT == NULL) continue;

            pmModel *modelPSF = pmModelFromPSF (source->modelEXT, psf);

            fprintf (f, "%f %f : ", source->modelEXT->params->data.F32[PM_PAR_XPOS], source->modelEXT->params->data.F32[PM_PAR_YPOS]);

            for (int i = 0; i < psf->params->n; i++) {
                if (psf->params->data[i] == NULL) continue;
                fprintf (f, "%f %f : ", source->modelEXT->params->data.F32[i], modelPSF->params->data.F32[i]);
            }
            fprintf (f, "%f %d\n", source->modelEXT->chisq, source->modelEXT->nIter);

            psFree(modelPSF);
        }
        fclose (f);
    }

    psFree (x);
    psFree (y);
    psFree (z);
    return true;
}


bool pmPSFFitShapeParams (pmPSF *psf, psArray *sources, psVector *x, psVector *y, psVector *srcMask) {

    // we are doing a robust fit.  after each pass, we drop points which are more deviant than
    // three sigma.  mask is currently updated for each pass.

    // The shape parameters (SXX, SXY, SYY) are strongly coupled.  We have to handle them very
    // carefully.  First, we convert them to the Ellipse Polarization terms (E0, E1, E2) for
    // each source and fit this set of parameters.  These values are less tightly coupled, but
    // are still inter-related.  The fitted values do a good job of constraining the major axis
    // and the position angle, but the minor axis is weakly measured.  When we apply the PSF
    // model to construct a source model, we convert the fitted values of E0,E1,E2 to the shape
    // parameters, with the constraint that the minor axis must be greater than a minimum
    // threshold.

    // convert the measured source shape paramters to polarization terms
    psVector *e0   = psVectorAlloc (sources->n, PS_TYPE_F32);
    psVector *e1   = psVectorAlloc (sources->n, PS_TYPE_F32);
    psVector *e2   = psVectorAlloc (sources->n, PS_TYPE_F32);
    psVector *mag  = psVectorAlloc (sources->n, PS_TYPE_F32);
    for (int i = 0; i < sources->n; i++) {
        pmSource *source = sources->data[i];
        if (source->modelEXT == NULL) continue;
        // XXX I am relying on the fact that none of the masked sources
        // have modelEXT set here.  perhaps use the value of psfTry->mask instead?

        psEllipsePol pol = pmPSF_ModelToFit (source->modelEXT->params->data.F32);

        e0->data.F32[i] = pol.e0;
        e1->data.F32[i] = pol.e1;
        e2->data.F32[i] = pol.e2;

        float flux = source->modelEXT->params->data.F32[PM_PAR_I0];
        mag->data.F32[i] = (flux > 0.0) ? -2.5*log(flux) : -100.0;
    }

    if (psf->psfTrendMode == PM_TREND_MAP) {
        float errorFloor = 0.0;
        float errorTotal = 0.0;
        float errorTotalMin = FLT_MAX;
        int entryMin = -1;

        psVector *dz = NULL;
        psVector *mask = psVectorAlloc (sources->n, PS_TYPE_U8);

        // check the fit residuals and increase Nx,Ny until the error is minimized
        // pmPSFParamTrend increases the number along the longer of x or y
        for (int i = 1; i < PS_MAX (psf->trendNx, psf->trendNy); i++) {

            psVectorInit (mask, 0);
            if (!pmPSFFitShapeParamsMap (psf, i, &errorFloor, &errorTotal, mask, x, y, mag, e0, e1, e2, dz)) {
                break;
            }

            // we do not have a good model for the error distributions on e0, e1, e2.
            // use the bright end scatter from the constant fit to set the scale for the
            // versions with 2D variation.  potentially scale by poisson errors...
            if (i == 1) {
                // if (psf->poissonErrorsParams) do something else..
                dz = psVectorAlloc (sources->n, PS_TYPE_F32);
                for (int i = 0; i < sources->n; i++) {
                    dz->data.F32[i] = errorFloor;
                }
            }

            // store the resulting errorTotal values and the scales, redo the best
            if (errorTotal < errorTotalMin) {
                errorTotalMin = errorTotal;
                entryMin = i;
            }
        }
        if (entryMin == -1) {
            psError (PS_ERR_UNKNOWN, true, "failed to find image map for shape params");
            return false;
        }

        // XXX supply the resulting mask values back to srcMask
        psVectorInit (mask, 0);
        if (!pmPSFFitShapeParamsMap (psf, entryMin, &errorFloor, &errorTotal, mask, x, y, mag, e0, e1, e2, dz)) {
            psAbort ("failed pmPSFFitShapeParamsMap on second pass?");
        }

        pmTrend2D *trend = psf->params->data[PM_PAR_E0];
        psf->trendNx = trend->map->map->numCols;
        psf->trendNy = trend->map->map->numRows;

        psFree (mask);
        psFree (dz);
    } else {

        // XXX iterate Nx, Ny based on scatter?
        // XXX we force the x & y order to be the same
        // modify the order to correspond to the actual number of matched stars:
        int order = PS_MAX (psf->trendNx, psf->trendNy);
        if ((sources->n < 15) && (order >= 3)) order = 2;
        if ((sources->n < 11) && (order >= 2)) order = 1;
        if ((sources->n <  8) && (order >= 1)) order = 0;
        if ((sources->n <  3)) {
            psError (PS_ERR_UNKNOWN, true, "failed to fit polynomial to shape params");
            return false;
        }
        psf->trendNx = order;
        psf->trendNy = order;

        // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each.
        // This way, the parameters masked by one of the fits will be applied to the others
        for (int i = 0; i < psf->psfTrendStats->clipIter; i++) {

            // XXX we are using the same stats structure on each pass: do we need to re-init it?
            bool status = true;
            status &= pmTrend2DFit (psf->params->data[PM_PAR_E0], srcMask, 0xff, x, y, e0, NULL);
            psTrace ("psModules.pmPSFtry", 4, "clipped E0 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e0->n);
            status &= pmTrend2DFit (psf->params->data[PM_PAR_E1], srcMask, 0xff, x, y, e1, NULL);
            psTrace ("psModules.pmPSFtry", 4, "clipped E1 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e1->n);
            status &= pmTrend2DFit (psf->params->data[PM_PAR_E2], srcMask, 0xff, x, y, e2, NULL);
            psTrace ("psModules.pmPSFtry", 4, "clipped E2 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e2->n);

            if (!status) {
                psError (PS_ERR_UNKNOWN, true, "failed to fit polynomial to shape params");
                return false;
            }
        }
    }

    // test dump of the psf parameters
    if (psTraceGetLevel("psModules.objects") >= 4) {
        FILE *f = fopen ("pol.dat", "w");
        fprintf (f, "# x y  :  e0obs e1obs e2obs  : e0fit e1fit e2fit : mask\n");
        for (int i = 0; i < e0->n; i++) {
            fprintf (f, "%f %f  :  %f %f %f  : %f %f %f  : %d\n",
                     x->data.F32[i], y->data.F32[i],
                     e0->data.F32[i], e1->data.F32[i], e2->data.F32[i],
                     pmTrend2DEval (psf->params->data[PM_PAR_E0], x->data.F32[i], y->data.F32[i]),
                     pmTrend2DEval (psf->params->data[PM_PAR_E1], x->data.F32[i], y->data.F32[i]),
                     pmTrend2DEval (psf->params->data[PM_PAR_E2], x->data.F32[i], y->data.F32[i]),
                     srcMask->data.U8[i]);
        }
        fclose (f);
    }

    psFree (e0);
    psFree (e1);
    psFree (e2);
    psFree (mag);
    return true;
}

// fit the shape variations as a psImageMap for the given scale factor
bool pmPSFFitShapeParamsMap (pmPSF *psf, int scale, float *errorFloor, float *errorTotal, psVector *mask, psVector *x, psVector *y, psVector *mag, psVector *e0obs, psVector *e1obs, psVector *e2obs, psVector *dz) {

    int Nx, Ny;

    // set the map scale to match the aspect ratio
    if (psf->fieldNx > psf->fieldNy) {
        Nx = scale;
        Ny = PS_MAX (1, Nx * (psf->fieldNy / psf->fieldNx));
    } else {
        Ny = scale;
        Nx = PS_MAX (1, Ny * (psf->fieldNx / psf->fieldNy));
    }

    // do we have enough sources for this fine of a grid?
    if (x->n < 3*Nx*Ny) {
        return false;
    }

    // the mask marks the values not used to calculate the ApTrend
    psVectorInit (mask, 0);

    // XXX check this against the exising type
    pmTrend2DMode psfTrendMode = PM_TREND_MAP;

    psImageBinning *binning = psImageBinningAlloc();
    binning->nXruff = Nx;
    binning->nYruff = Ny;
    binning->nXfine = psf->fieldNx;
    binning->nYfine = psf->fieldNy;
    psImageBinningSetScale (binning, PS_IMAGE_BINNING_CENTER);
    psImageBinningSetSkipByOffset (binning, psf->fieldXo, psf->fieldYo);

    psFree (psf->params->data[PM_PAR_E0]);
    psFree (psf->params->data[PM_PAR_E1]);
    psFree (psf->params->data[PM_PAR_E2]);

    int nIter = psf->psfTrendStats->clipIter;
    psf->psfTrendStats->clipIter = 1;
    psf->params->data[PM_PAR_E0] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
    psf->params->data[PM_PAR_E1] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
    psf->params->data[PM_PAR_E2] = pmTrend2DNoImageAlloc (psfTrendMode, binning, psf->psfTrendStats);
    psFree (binning);

    // we run 'clipIter' cycles clipping in each of x and y, with only one iteration each.
    // This way, the parameters masked by one of the fits will be applied to the others
    for (int i = 0; i < nIter; i++) {
        // XXX we are using the same stats structure on each pass: do we need to re-init it?
        pmTrend2DFit (psf->params->data[PM_PAR_E0], mask, 0xff, x, y, e0obs, dz);
        psTrace ("psModules.objects", 5, "clipped E0 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e0obs->n);
        pmTrend2DFit (psf->params->data[PM_PAR_E1], mask, 0xff, x, y, e1obs, dz);
        psTrace ("psModules.objects", 5, "clipped E1 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e1obs->n);
        pmTrend2DFit (psf->params->data[PM_PAR_E2], mask, 0xff, x, y, e2obs, dz);
        psTrace ("psModules.objects", 5, "clipped E2 : keeping %ld of %ld\n", psf->psfTrendStats->clippedNvalues, e2obs->n);
    }
    psf->psfTrendStats->clipIter = nIter; // restore default setting

    // construct the fitted values and the residuals
    psVector *e0fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E0], x, y);
    psVector *e1fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E1], x, y);
    psVector *e2fit = pmTrend2DEvalVector (psf->params->data[PM_PAR_E2], x, y);

    psVector *e0res = (psVector *) psBinaryOp (NULL, (void *) e0obs, "-", (void *) e0fit);
    psVector *e1res = (psVector *) psBinaryOp (NULL, (void *) e1obs, "-", (void *) e1fit);
    psVector *e2res = (psVector *) psBinaryOp (NULL, (void *) e2obs, "-", (void *) e2fit);

    // measure errors on the map pixels
    pmTrend2D *trend;

    trend = psf->params->data[PM_PAR_E0];
    float mapErrorSum = 0.0;
    float mapError = 0.0;
    for (int iy = 0; iy < trend->map->error->numRows; iy++) {
        for (int ix = 0; ix < trend->map->error->numCols; ix++) {
            mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
        }
    }
    psTrace ("psModules.objects", 4, "E0 error: %f\n", sqrt(mapError));
    mapErrorSum += mapError;

    trend = psf->params->data[PM_PAR_E1];
    mapError = 0.0;
    for (int iy = 0; iy < trend->map->error->numRows; iy++) {
        for (int ix = 0; ix < trend->map->error->numCols; ix++) {
            mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
        }
    }
    psTrace ("psModules.objects", 4, "E1 error: %f\n", sqrt(mapError));
    mapErrorSum += mapError;

    trend = psf->params->data[PM_PAR_E2];
    mapError = 0.0;
    for (int iy = 0; iy < trend->map->error->numRows; iy++) {
        for (int ix = 0; ix < trend->map->error->numCols; ix++) {
            mapError += PS_SQR (trend->map->error->data.F32[iy][ix]);
        }
    }
    psTrace ("psModules.objects", 4, "E2 error: %f\n", sqrt(mapError));
    mapErrorSum += mapError;

    psTrace ("psModules.objects", PS_LOG_INFO, "total map error: %f\n", sqrt(mapErrorSum));

    // measure systematic errorFloor & systematic / photon scale factor
    // XXX this is a bit arbitrary, but it forces ~3 stars from the bright bin per spatial bin
    int nGroup = PS_MAX (3*Nx*Ny, 10);
    pmPSFShapeParamsErrors (errorFloor, mag, e0res, e1res, e2res, mask, nGroup,
                            psStatsStdevOption(psf->psfTrendStats->options));

    *errorTotal = sqrt(PS_SQR(*errorFloor) + mapErrorSum);

    psLogMsg ("psphot.psftry", PS_LOG_INFO, "result of %d x %d grid (%d stars per bin)\n", Nx, Ny, nGroup);
    psLogMsg ("psphot.psftry", PS_LOG_INFO, "systematic scatter floor: %f, error map total: %f\n", *errorFloor, *errorTotal);

    psFree (e0fit);
    psFree (e1fit);
    psFree (e2fit);

    psFree (e0res);
    psFree (e1res);
    psFree (e2res);

    return true;
}

// calculate the minimum scatter of the parameters
bool pmPSFShapeParamsErrors(float *errorFloor, psVector *mag, psVector *e0res, psVector *e1res,
                            psVector *e2res, psVector *mask, int nGroup, psStatsOptions stdevOpt)
{

    psStats *statsS = psStatsAlloc(stdevOpt);

    // measure the trend in bins with 10 values each; if < 10 total, use them all
    int nBin = PS_MAX (mag->n / nGroup, 1);

    // use mag to group parameters in sequence
    psVector *index = psVectorSortIndex (NULL, mag);

    // subset vectors for mag and dap values within the given range
    psVector *dE0subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
    psVector *dE1subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
    psVector *dE2subset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
    psVector *mkSubset  = psVectorAllocEmpty (nGroup, PS_TYPE_U8);

    int n = 0;
    float min = INFINITY;               // Minimum error
    for (int i = 0; i < nBin; i++) {
        int j;
        for (j = 0; (j < nGroup) && (n < mag->n); j++, n++) {
            int N = index->data.U32[n];
            dE0subset->data.F32[j] = e0res->data.F32[N];
            dE1subset->data.F32[j] = e1res->data.F32[N];
            dE2subset->data.F32[j] = e2res->data.F32[N];

            mkSubset->data.U8[j]   = mask->data.U8[N];
        }
        dE0subset->n = j;
        dE1subset->n = j;
        dE2subset->n = j;
        mkSubset->n = j;

        // calculate the root-mean-square of E0, E1, E2
        float dEsquare = 0.0;
        psStatsInit (statsS);
        psVectorStats (statsS, dE0subset, NULL, mkSubset, 0xff);
        dEsquare += PS_SQR(psStatsGetValue(statsS, stdevOpt));

        psStatsInit (statsS);
        psVectorStats (statsS, dE1subset, NULL, mkSubset, 0xff);
        dEsquare += PS_SQR(psStatsGetValue(statsS, stdevOpt));

        psStatsInit (statsS);
        psVectorStats (statsS, dE2subset, NULL, mkSubset, 0xff);
        dEsquare += PS_SQR(psStatsGetValue(statsS, stdevOpt));

        if (isfinite(dEsquare)) {
            float err = sqrtf(dEsquare);
            if (err < min) {
                min = err;
            }
        }
    }
    psFree (dE0subset);
    psFree (dE1subset);
    psFree (dE2subset);
    psFree (mkSubset);

    psFree(index);

    psFree(statsS);

    return true;
}
