Index: branches/simtest_nebulous_branches/psphot/src/Makefile.am
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/Makefile.am	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/Makefile.am	(revision 27840)
@@ -25,5 +25,6 @@
 libpsphot_la_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
 
-bin_PROGRAMS = psphot psphotTest psphotMomentsStudy
+bin_PROGRAMS = psphot psphotForced psphotMakePSF psphotStack
+# bin_PROGRAMS = psphotPetrosianStudy psphotTest psphotMomentsStudy 
 
 psphot_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
@@ -31,34 +32,89 @@
 psphot_LDADD = libpsphot.la
 
-psphotTest_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
-psphotTest_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
-psphotTest_LDADD = libpsphot.la
-
-psphotMomentsStudy_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
-psphotMomentsStudy_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
-psphotMomentsStudy_LDADD = libpsphot.la
-
+psphotForced_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psphotForced_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psphotForced_LDADD = libpsphot.la
+
+psphotMakePSF_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psphotMakePSF_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psphotMakePSF_LDADD = libpsphot.la
+
+psphotStack_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+psphotStack_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+psphotStack_LDADD = libpsphot.la
+
+# psphotMomentsStudy_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+# psphotMomentsStudy_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+# psphotMomentsStudy_LDADD = libpsphot.la
+
+# psphotPetrosianStudy_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+# psphotPetrosianStudy_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+# psphotPetrosianStudy_LDADD = libpsphot.la
+
+# psphotTest_CFLAGS = $(PSPHOT_CFLAGS) $(PSMODULE_CFLAGS) $(PSLIB_CFLAGS)
+# psphotTest_LDFLAGS = $(PSPHOT_LIBS) $(PSMODULE_LIBS) $(PSLIB_LIBS)
+# psphotTest_LDADD = libpsphot.la
+
+# standard psphot for generic photometry
 psphot_SOURCES = \
         psphot.c                \
 	psphotArguments.c	\
-	psphotCleanup.c	   	\
+	psphotParseCamera.c     \
 	psphotImageLoop.c	\
 	psphotMosaicChip.c	\
-	psphotParseCamera.c
-
-psphotDetect_SOURCES =            \
-        psphotDetect.c            \
-	psphotDetectArguments.c	  \
-	psphotDetectParseCamera.c \
-	psphotDetectImageLoop.c	  \
-	psphotDetectReadout.c	  \
-	psphotMosaicChip.c	  \
-	psphotCleanup.c
-
-psphotTest_SOURCES = \
-        psphotTest.c
-
-psphotMomentsStudy_SOURCES = \
-        psphotMomentsStudy.c
+	psphotCleanup.c
+
+# forced photometry of specified positions given a specified psf
+psphotForced_SOURCES = \
+        psphotForced.c             \
+	psphotForcedArguments.c	   \
+	psphotForcedImageLoop.c	   \
+	psphotForcedReadout.c	   \
+	psphotParseCamera.c        \
+	psphotMosaicChip.c	   \
+	psphotCleanup.c
+
+# a psphot-variant that simply generates the PSF model
+psphotMakePSF_SOURCES = \
+        psphotMakePSF.c            \
+	psphotMakePSFArguments.c   \
+	psphotMakePSFImageLoop.c   \
+	psphotMakePSFReadout.c	   \
+	psphotParseCamera.c        \
+	psphotMosaicChip.c	   \
+	psphotCleanup.c
+
+# a psphot-variant for stack photometry
+psphotStack_SOURCES = \
+        psphotStack.c                 \
+	psphotStackArguments.c        \
+	psphotStackParseCamera.c      \
+	psphotStackImageLoop.c        \
+	psphotStackReadout.c	      \
+	psphotStackChisqImage.c	      \
+	psphotFitSourcesLinearStack.c \
+	psphotSourceMatch.c           \
+	psphotCleanup.c
+
+
+
+# # psphot analysis of the detectability of specified positions
+# psphotDetect_SOURCES =            \
+#         psphotDetect.c            \
+# 	psphotDetectArguments.c	  \
+# 	psphotDetectParseCamera.c \
+# 	psphotDetectImageLoop.c	  \
+# 	psphotDetectReadout.c	  \
+# 	psphotMosaicChip.c	  \
+# 	psphotCleanup.c
+
+# psphotTest_SOURCES = \
+#         psphotTest.c
+# 
+# psphotMomentsStudy_SOURCES = \
+#         psphotMomentsStudy.c
+# 
+# psphotPetrosianStudy_SOURCES = \
+#         psphotPetrosianStudy.c
 
 libpsphot_la_SOURCES = \
@@ -104,12 +160,6 @@
 	psphotExtendedSourceAnalysis.c \
 	psphotExtendedSourceFits.c     \
-	psphotRadialProfile.c	       \
-	psphotPetrosian.c	       \
-	psphotIsophotal.c	       \
-	psphotAnnuli.c		       \
-	psphotKron.c		       \
 	psphotKernelFromPSF.c	       \
 	psphotPSFConvModel.c	       \
-	psphotModelTest.c	       \
 	psphotFitSet.c		       \
 	psphotSourceFreePixels.c       \
@@ -117,4 +167,5 @@
 	psphotMergeSources.c	       \
 	psphotLoadPSF.c	               \
+	psphotLoadSRCTEXT.c            \
 	psphotReadoutCleanup.c	       \
 	psphotSourcePlots.c	       \
@@ -128,7 +179,30 @@
 	psphotCheckStarDistribution.c  \
 	psphotThreadTools.c  	       \
-	psphotAddNoise.c
-
-# dropped? psphotGrowthCurve.c
+	psphotAddNoise.c               \
+	psphotRadialProfile.c	       \
+        psphotRadialProfileByAngles.c  \
+	psphotRadiiFromProfiles.c      \
+        psphotEllipticalContour.c      \
+        psphotEllipticalProfile.c      \
+	psphotRadialBins.c	       \
+	psphotPetrosian.c	       \
+        psphotPetrosianRadialBins.c    \
+        psphotPetrosianStats.c         \
+        psphotPetrosianVisual.c        \
+	psphotEfficiency.c
+
+# XXX need to fix this for the new apis
+#	psphotModelTest.c	       
+
+# re-instate these
+#	psphotIsophotal.c	       \
+#	psphotAnnuli.c		       \
+#	psphotKron.c		       \
+#
+
+# test versions
+#       psphotPetrosianProfile.c       \
+#       psphotPetrosianAnalysis.c      \
+#
 
 include_HEADERS = \
Index: branches/simtest_nebulous_branches/psphot/src/psphot.h
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphot.h	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphot.h	(revision 27840)
@@ -12,4 +12,12 @@
 
 #define PSPHOT_RECIPE_PSF_FAKE_ALLOW "PSF.FAKE.ALLOW" // Name for recipe component permitting fake PSFs
+
+// pmPCMData : PSF Convolved Model data storage structure
+typedef struct {
+    psImage *model;
+    psArray *dmodels;
+    psImage *modelConv;
+    psArray *dmodelsConv;
+} pmPCMData;
 
 // top-level psphot functions
@@ -29,5 +37,7 @@
 bool            psphotReadoutMinimal(pmConfig *config, const pmFPAview *view);
 
-bool            psphotReadoutCleanup (pmConfig *config, pmReadout *readout, psMetadata *recipe, pmDetections *detections, pmPSF *psf, psArray *sources);
+bool            psphotReadoutCleanup (pmConfig *config, const pmFPAview *view);
+bool            psphotReadoutCleanupReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
 bool            psphotDefineFiles (pmConfig *config, pmFPAfile *input);
 void            psphotFilesActivate(pmConfig *config, bool state);
@@ -38,42 +48,122 @@
 // XXX test functions
 psArray        *psphotFakeSources (void);
+bool            psphotMaskCosmicRayFootprintCheck (psArray *sources);
 
 // psphotReadout functions
-bool            psphotModelBackground (pmConfig *config, const pmFPAview *view, const char *filename);
-bool            psphotSubtractBackground (pmConfig *config, const pmFPAview *view, const char *filename) ;
-pmDetections   *psphotFindDetections (pmDetections *detections, pmReadout *readout, psMetadata *recipe);
-
-bool            psphotRoughClass (pmReadout *readout, psArray *sources, psMetadata *recipe, const bool findPsfClump);
-bool            psphotBasicDeblend (psArray *sources, psMetadata *recipe);
-pmPSF          *psphotChoosePSF (pmReadout *readout, psArray *sources, psMetadata *recipe);
-bool            psphotPSFstats (pmReadout *readout, psMetadata *recipe, pmPSF *psf);
-bool            psphotMomentsStats (pmReadout *readout, psMetadata *recipe, psArray *sources);
-bool            psphotFitSourcesLinear (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final);
-bool            psphotReplaceUnfitSources (psArray *sources, psImageMaskType maskVal);
-
-bool            psphotReplaceAllSources (psArray *sources, psMetadata *recipe);
-bool            psphotRemoveAllSources (psArray *sources, psMetadata *recipe);
-
-bool            psphotBlendFit (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf);
+bool            psphotAddPhotcode (pmConfig *config, const pmFPAview *view);
+bool            psphotAddPhotcodeReadout (pmConfig *config, const pmFPAview *view, const char *filerule, int index);
+
+bool            psphotSetMaskAndVariance (pmConfig *config, const pmFPAview *view);
+bool            psphotSetMaskAndVarianceReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotModelBackground (pmConfig *config, const pmFPAview *view);
+bool            psphotModelBackgroundReadoutFileIndex (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotSubtractBackground (pmConfig *config, const pmFPAview *view);
+bool            psphotSubtractBackgroundReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotFindDetections (pmConfig *config, const pmFPAview *view, bool firstPass);
+bool            psphotFindDetectionsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool firstPass);
+
+bool            psphotSourceStats (pmConfig *config, const pmFPAview *view, bool setWindow);
+bool            psphotSourceStatsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool setWindow);
+
+bool            psphotDeblendSatstars (pmConfig *config, const pmFPAview *view);
+bool            psphotDeblendSatstarsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotBasicDeblend (pmConfig *config, const pmFPAview *view);
+bool            psphotBasicDeblendReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotRoughClass (pmConfig *config, const pmFPAview *view);
+bool            psphotRoughClassReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+bool            psphotRoughClassRegion (int nRegion, psRegion *region, psArray *sources, psMetadata *analysis, psMetadata *recipe, const bool havePSF);
+
+bool            psphotImageQuality (pmConfig *config, const pmFPAview *view);
+bool            psphotImageQualityReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotChoosePSF (pmConfig *config, const pmFPAview *view);
+bool            psphotChoosePSFReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotGuessModels (pmConfig *config, const pmFPAview *view);
+bool            psphotGuessModelsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotMergeSources (pmConfig *config, const pmFPAview *view);
+bool            psphotMergeSourcesReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotFitSourcesLinear (pmConfig *config, const pmFPAview *view, bool final);
+bool            psphotFitSourcesLinearReadout (psMetadata *recipe, pmReadout *readout, psArray *sources, pmPSF *psf, bool final);
+
+bool            psphotSourceSize (pmConfig *config, const pmFPAview *view, bool getPSFsize);
+bool            psphotSourceSizeReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool getPSFsize);
+
+bool            psphotBlendFit (pmConfig *config, const pmFPAview *view);
+bool            psphotBlendFitReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
 bool            psphotBlendFit_Threaded (psThreadJob *job);
 
-psArray        *psphotSourceStats (pmConfig *config, pmReadout *readout, pmDetections *detections);
-bool            psphotSourceStats_Threaded (psThreadJob *job);
-
-bool            psphotGuessModels (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf);
-bool            psphotGuessModel_Threaded (psThreadJob *job);
-
-bool            psphotMagnitudes (pmConfig *config, pmReadout *readout, const pmFPAview *view, psArray *sources, pmPSF *psf);
+bool            psphotReplaceAllSources (pmConfig *config, const pmFPAview *view);
+bool            psphotReplaceAllSourcesReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotAddNoise (pmConfig *config, const pmFPAview *view);
+bool            psphotSubNoise (pmConfig *config, const pmFPAview *view);
+bool            psphotAddOrSubNoise (pmConfig *config, const pmFPAview *view, bool add);
+bool            psphotAddOrSubNoiseReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool add);
+
+bool            psphotExtendedSourceAnalysis (pmConfig *config, const pmFPAview *view);
+bool            psphotExtendedSourceAnalysisReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotExtendedSourceFits (pmConfig *config, const pmFPAview *view);
+bool            psphotExtendedSourceFitsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotApResid (pmConfig *config, const pmFPAview *view);
+bool            psphotApResidReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
+
+bool            psphotMagnitudes (pmConfig *config, const pmFPAview *view);
+bool            psphotMagnitudesReadout(pmConfig *config, psMetadata *recipe, const pmFPAview *view, pmReadout *readout, psArray *sources, pmPSF *psf);
 bool            psphotMagnitudes_Threaded (psThreadJob *job);
+
+bool            psphotEfficiency (pmConfig *config, const pmFPAview *view);
+bool            psphotEfficiencyReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe);
 
 bool            psphotPSFWeights(pmConfig *config, pmReadout *readout, const pmFPAview *view, psArray *sources);
 bool            psphotPSFWeights_Threaded (psThreadJob *job);
 
-bool            psphotApResid (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf);
-bool            psphotApResidMags_Threaded (psThreadJob *job);
-
 bool            psphotSkyReplace (pmConfig *config, const pmFPAview *view);
-bool            psphotExtendedSourceAnalysis (pmReadout *readout, psArray *sources, psMetadata *recipe);
-bool            psphotExtendedSourceFits (pmReadout *readout, psArray *sources, psMetadata *recipe);
+bool            psphotSkyReplaceReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+bool            psphotSourceFreePixels (pmConfig *config, const pmFPAview *view);
+bool            psphotSourceFreePixelsReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index);
+
+// in psphotSourceStats.c:
+bool            psphotSourceStats_Threaded (psThreadJob *job);
+bool            psphotSourceStatsUpdate (psArray *sources, pmConfig *config, pmReadout *readout);
+bool            psphotSetMomentsWindow (psMetadata *recipe, psMetadata *analysis, psArray *sources);
+
+// in psphotChoosePSF.c:
+bool            psphotPSFstats (pmReadout *readout, pmPSF *psf);
+bool            psphotMomentsStats (pmReadout *readout, psArray *sources);
+
+// in psphotGuessModel.c
+bool            psphotGuessModel_Threaded (psThreadJob *job);
+
+// in psphotMergeSources.c:
+bool            psphotLoadExtSources (pmConfig *config, const pmFPAview *view);
+psArray        *psphotLoadPSFSources (pmConfig *config, const pmFPAview *view);
+bool            psphotRepairLoadedSources (pmConfig *config, const pmFPAview *view);
+bool            psphotCheckExtSources (pmConfig *config, const pmFPAview *view);
+
+// generate the detection structure for the supplied array of sources
+bool            psphotDetectionsFromSources (pmConfig *config, const pmFPAview *view, psArray *sources);
+
+// generate the detection structure for the supplied array of sources
+bool            psphotSetSourceParams (pmConfig *config, psArray *sources, pmPSF *psf);
+
+// in psphotModelBackground.c:
+// Create a background model for a readout, without saving the result as a pmFPAfile on config->files.  Otherwise identical to psphotModelBackgroundFileIndex.
+psImage        *psphotModelBackgroundReadoutNoFile(pmReadout *readout, const pmConfig *config);
+psImageBinning *psphotBackgroundBinning(const psImage *image, const pmConfig *config);
+
+// in psphotReplaceUnfit.c:
+bool            psphotRemoveAllSources (const psArray *sources, const psMetadata *recipe);
+bool            psphotReplaceUnfitSources (psArray *sources, psImageMaskType maskVal);
 
 // thread-related:
@@ -86,29 +176,24 @@
 psImage        *psphotSignificanceImage (pmReadout *readout, psMetadata *recipe, const int pass, psImageMaskType maskVal);
 psArray        *psphotFindPeaks (psImage *significance, pmReadout *readout, psMetadata *recipe, const float threshold, const int nMax);
-bool            psphotFindFootprints (pmDetections *detections, psImage *significance, pmReadout *readout, psMetadata *recipe, const int pass, psImageMaskType maskVal);
-psErrorCode     psphotCullPeaks(const psImage *img, const psImage *weight, const psMetadata *recipe, psArray *footprints);
-
-// generate the detection structure for the supplied array of sources
-pmDetections   *psphotDetectionsFromSources (pmConfig *config, psArray *sources);
-
-// used by ApResid
-bool            psphotMagErrorScale (float *errorScale, float *errorFloor, psVector *dMag, psVector *dap, psVector *mask, int nGroup);
-bool            psphotApResidTrend (pmReadout *readout, pmPSF *psf, int Npsf, int scale, float *errorScale, float *errorFloor, psVector *mask, psVector *xPos, psVector *yPos, psVector *apResid, psVector *dMag);
+bool            psphotFindFootprints (pmDetections *detections, psImage *significance, pmReadout *readout, psMetadata *recipe, const float threshold, const int pass, psImageMaskType maskVal);
+psErrorCode     psphotCullPeaks(const pmReadout *readout, const psMetadata *recipe, psArray *footprints);
+
+// in psphotApResid.c:
+pmTrend2D      *psphotApResidTrend (float *apResidSysErr, pmReadout *readout, int Nx, int Ny, psVector *xPos, psVector *yPos, psVector *apResid, psVector *dMag);
+bool            psphotApResidMags_Threaded (psThreadJob *job);
 
 // basic support functions
 void            psphotModelClassInit (void);
 bool            psphotGrowthCurve (pmReadout *readout, pmPSF *psf, bool ignore, psImageMaskType maskVal);
-bool            psphotSetMaskAndVariance (pmConfig *config, pmReadout *readout, psMetadata *recipe);
-void            psphotSourceFreePixels (psArray *sources);
 
 // functions to set the correct source pixels
-bool            psphotInitRadiusPSF (const psMetadata *recipe, const pmModelType type);
+bool            psphotInitRadiusPSF(const psMetadata *recipe, const psMetadata *analysis, const pmModelType type);
+
 bool            psphotCheckRadiusPSF (pmReadout *readout, pmSource *source, pmModel *model, psImageMaskType markVal);
 bool            psphotCheckRadiusPSFBlend (pmReadout *readout, pmSource *source, pmModel *model, psImageMaskType markVal, float dR);
 bool            psphotInitRadiusEXT (psMetadata *recipe, pmModelType type);
 bool            psphotCheckRadiusEXT (pmReadout *readout, pmSource *source, pmModel *model, psImageMaskType markVal);
-
-// output functions
-bool            psphotAddPhotcode (psMetadata *recipe, pmConfig *config, const pmFPAview *view, const char *filerule);
+float           psphotSetRadiusEXT (pmReadout *readout, pmSource *source, psImageMaskType markVal);
+
 bool            psphotDumpMoments (psMetadata *recipe, psArray *sources);
 psMetadata     *psphotDefineHeader (psMetadata *recipe);
@@ -141,23 +226,12 @@
 bool            psphotFitSummary (void);
 
-bool            psphotMergeSources (psArray *oldSources, psArray *newSources);
-bool            psphotLoadExtSources (pmConfig *config, const pmFPAview *view, psArray *sources);
-psArray        *psphotLoadPSFSources (pmConfig *config, const pmFPAview *view);
-
-pmPSF          *psphotLoadPSF (pmConfig *config, const pmFPAview *view, psMetadata *recipe);
+bool            psphotLoadPSF (pmConfig *config, const pmFPAview *view);
+bool            psphotLoadPSFReadout (pmConfig *config, const pmFPAview *view, const char *outFilename, const char *inFilename, int index);
+
 bool            psphotSetHeaderNstars (psMetadata *recipe, psArray *sources);
-bool            psphotAddNoise (pmReadout *readout, psArray *sources, psMetadata *recipe);
-bool            psphotSubNoise (pmReadout *readout, psArray *sources, psMetadata *recipe);
-bool            psphotAddOrSubNoise (pmReadout *readout, psArray *sources, psMetadata *recipe, bool add);
 bool            psphotRadialPlot (int *kapa, const char *filename, pmSource *source);
 bool            psphotSourcePlots (pmReadout *readout, psArray *sources, psMetadata *recipe);
 bool            psphotMosaicSubimage (psImage *outImage, pmSource *source, int Xo, int Yo, int DX, int DY, bool normalize);
 
-bool            psphotAddWithTest (pmSource *source, bool useState, psImageMaskType maskVal);
-bool            psphotSubWithTest (pmSource *source, bool useState, psImageMaskType maskVal);
-bool            psphotSetState (pmSource *source, bool curState, psImageMaskType maskVal);
-bool            psphotDeblendSatstars (pmReadout *readout, psArray *sources, psMetadata *recipe);
-bool            psphotSourceSize (pmConfig *config, pmReadout *readout, psArray *sources, psMetadata *recipe, long first);
-
 bool            psphotMakeResiduals (psArray *sources, psMetadata *recipe, pmPSF *psf, psImageMaskType maskVal);
 
@@ -166,42 +240,57 @@
 psKernel       *psphotKernelFromPSF (pmSource *source, int nPix);
 
-bool            psphotRadialProfile (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
-bool            psphotPetrosian (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
-bool            psphotIsophotal (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
-bool            psphotAnnuli (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
-bool            psphotKron (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
+// functions related to extended source analysis
+bool            psphotRadialProfile (pmSource *source, psMetadata *recipe, float skynoise, psImageMaskType maskVal);
+bool            psphotRadialProfilesByAngles (pmSource *source, int Nsec, float Rmax);
+float           psphotRadiusFromProfile (pmSource *source, psVector *radius, psVector *flux, float fluxMin, float fluxMax);
+bool            psphotRadiiFromProfiles (pmSource *source, float fluxMin, float fluxMax);
+bool            psphotEllipticalProfile (pmSource *source, bool RAW_RADIUS);
+bool            psphotEllipticalContour (pmSource *source);
 
 // psphotVisual functions
-bool psphotVisualShowImage (pmReadout *readout);
-bool psphotVisualShowBackground (pmConfig *config, const pmFPAview *view, pmReadout *readout);
-bool psphotVisualShowSignificance (psImage *image);
-bool psphotVisualShowPeaks (pmDetections *detections);
-bool psphotVisualShowFootprints (pmDetections *detections);
-bool psphotVisualShowMoments (psArray *sources);
-bool psphotVisualPlotMoments (psMetadata *recipe, psArray *sources);
-bool psphotVisualShowRoughClass (psArray *sources);
-bool psphotVisualShowPSFStars (psMetadata *recipe, pmPSF *psf, psArray *sources);
-bool psphotVisualShowSatStars (psMetadata *recipe, pmPSF *psf, psArray *sources);
-bool psphotVisualShowPSFModel (pmReadout *readout, pmPSF *psf);
-bool psphotVisualPlotRadialProfile (int myKapa, pmSource *source, psImageMaskType maskVal);
-bool psphotVisualPlotRadialProfiles (psMetadata *recipe, psArray *sources);
-bool psphotVisualShowFlags (psArray *sources);
-bool psphotVisualShowSourceSize (pmReadout *readout, psArray *sources);
-bool psphotVisualShowResidualImage (pmReadout *readout);
-bool psphotVisualPlotApResid (psArray *sources);
-bool psphotVisualPlotSourceSize (psArray *sources);
-
-bool psphotImageQuality (psMetadata *recipe, psArray *sources);
+bool            psphotVisualShowImage (pmReadout *readout);
+bool            psphotVisualShowBackground (pmConfig *config, const pmFPAview *view, pmReadout *readout);
+bool            psphotVisualShowSignificance (psImage *image, float min, float max);
+bool            psphotVisualShowPeaks (pmDetections *detections);
+bool            psphotVisualShowFootprints (pmDetections *detections);
+bool            psphotVisualShowMoments (psArray *sources);
+bool            psphotVisualPlotMoments (psMetadata *recipe, psMetadata *analysis, psArray *sources);
+bool            psphotVisualShowRoughClass (psArray *sources);
+bool            psphotVisualShowPSFStars (psMetadata *recipe, pmPSF *psf, psArray *sources);
+bool            psphotVisualShowSatStars (psMetadata *recipe, pmPSF *psf, psArray *sources);
+bool            psphotVisualShowPSFModel (pmReadout *readout, pmPSF *psf);
+bool            psphotVisualPlotRadialProfile (int myKapa, pmSource *source, psImageMaskType maskVal);
+bool            psphotVisualPlotRadialProfiles (psMetadata *recipe, psArray *sources);
+bool            psphotVisualShowFlags (psArray *sources);
+bool            psphotVisualShowSourceSize (pmReadout *readout, psArray *sources);
+bool            psphotVisualShowResidualImage (pmReadout *readout);
+bool            psphotVisualPlotApResid (psArray *sources, float mean, float error);
+bool            psphotVisualPlotChisq (psArray *sources);
+bool            psphotVisualPlotSourceSize (psMetadata *recipe, psMetadata *analysis, psArray *sources);
+bool            psphotVisualShowPetrosians (psArray *sources);
+bool            psphotVisualEraseOverlays (int channel, char *overlay);
+
+bool psphotPetrosian (pmSource *source, psMetadata *recipe, float skynoise, psImageMaskType maskVal);
+bool psphotPetrosianRadialBins (pmSource *source, float radiusMax, float skynoise);
+bool psphotPetrosianStats (pmSource *source);
+
+// currently disabled:
+// bool            psphotIsophotal (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
+// bool            psphotAnnuli (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
+// bool            psphotKron (pmSource *source, psMetadata *recipe, psImageMaskType maskVal);
+
+// XXX visualization functions related to radial profiles (disabled)
+bool psphotPetrosianVisualProfileByAngle (psVector *radius, psVector *flux);
+bool psphotPetrosianVisualProfileRadii (psVector *radius, psVector *flux, psVector *radiusBin, psVector *fluxBin, float peakFlux, float RadiusRef);
+bool psphotPetrosianVisualEllipticalContour (pmSourceRadialFlux *radFlux, pmSourceExtendedPars *extpars);
+bool psphotPetrosianVisualStats (psVector *radBin, psVector *fluxBin,
+                               psVector *refRadius, psVector *meanSB,
+                               psVector *petRatio, psVector *petRatioErr, psVector *fluxSum,
+                               float petRadius, float ratioForRadius,
+                               float petFlux, float radiusForFlux);
+
+bool psphotRadialBins (psMetadata *recipe, pmSource *source, float radiusMax, float skynoise);
 
 // structures & functions to support psf-convolved model fitting
-
-// pmPCMData : PSF Convolved Model data storage structure
-typedef struct {
-    psImage *model;
-    psArray *dmodels;
-    psImage *modelConv;
-    psArray *dmodelsConv;
-} pmPCMData;
-
 
 // psf-convolved model fitting
@@ -241,14 +330,40 @@
 bool psphotDumpPSFStars (pmReadout *readout, pmPSFtry *try, float radius, psImageMaskType maskVal, psImageMaskType markVal);
 
+bool psphotLoadSRCTEXT (pmFPA *fpa, pmConfig *config);
+
 bool psphotCheckStarDistribution (psArray *sources, psArray *stars, pmPSFOptions *options);
 int psphotSupplementStars (psArray *stars, psArray *sources, psImageBinning *binning, int ix, int iy);
 
-
-// Create a background model for a readout
-// Essentially identical to psphotModelBackground, but with a more convenient API for outsiders.
-psImage *psphotBackgroundModel(pmReadout *ro, // Readout for which to generate background model
-                               const pmConfig *config // Configuration
-    );
-
+pmConfig *psphotForcedArguments(int argc, char **argv);
+bool psphotForcedImageLoop (pmConfig *config);
+bool psphotForcedReadout(pmConfig *config, const pmFPAview *view);
+
+pmConfig *psphotMakePSFArguments(int argc, char **argv);
+bool psphotMakePSFImageLoop (pmConfig *config);
+bool psphotMakePSFReadout(pmConfig *config, const pmFPAview *view);
+
+/**** psphotStack prototypes ****/
+
+pmConfig *psphotStackArguments(int argc, char **argv);
+bool psphotStackParseCamera (pmConfig *config);
+bool psphotStackImageLoop (pmConfig *config);
+bool psphotStackReadout (pmConfig *config, const pmFPAview *view);
+bool psphotStackChisqImage (pmConfig *config, const pmFPAview *view);
+bool psphotStackChisqImageAddReadout(const pmConfig *config, // Configuration
+				     const pmFPAview *view,
+				     pmReadout **chiReadout,
+				     char *filename, 
+				     int index);
+
+bool psphotStackRemoveChisqFromInputs (pmConfig *config);
+bool pmFPAfileRemoveSingle(psMetadata *files, const char *name, int num);
+
+psArray *psphotMatchSources (pmConfig *config, const pmFPAview *view);
+bool psphotMatchSourcesReadout (psArray *objects, pmConfig *config, const pmFPAview *view, char *filename, int index);
+bool psphotMatchSourcesToObjects (psArray *objects, psArray *sources, float RADIUS);
+
+bool psphotFitSourcesLinearStack (pmConfig *config, psArray *objects, bool final);
+int pmPhotObjSortBySN (const void **a, const void **b);
+int pmPhotObjSortByX (const void **a, const void **b);
 
 #endif
Index: branches/simtest_nebulous_branches/psphot/src/psphotAddNoise.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotAddNoise.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotAddNoise.c	(revision 27840)
@@ -1,13 +1,35 @@
 # include "psphotInternal.h"
 
-bool psphotAddNoise (pmReadout *readout, psArray *sources, psMetadata *recipe) {
-  return psphotAddOrSubNoise (readout, sources, recipe, true);
+bool psphotAddNoise (pmConfig *config, const pmFPAview *view) {
+    return psphotAddOrSubNoise (config, view, true);
 }
 
-bool psphotSubNoise (pmReadout *readout, psArray *sources, psMetadata *recipe) {
-  return psphotAddOrSubNoise (readout, sources, recipe, false);
+bool psphotSubNoise (pmConfig *config, const pmFPAview *view) {
+    return psphotAddOrSubNoise (config, view, false);
 }
 
-bool psphotAddOrSubNoise (pmReadout *readout, psArray *sources, psMetadata *recipe, bool add) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotAddOrSubNoise (pmConfig *config, const pmFPAview *view, bool add)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotAddOrSubNoiseReadout (config, view, "PSPHOT.INPUT", i, recipe, add)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on to modify noise for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotAddOrSubNoiseReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool add) {
 
     bool status = false;
@@ -16,7 +38,18 @@
     psEllipseAxes axes;
 
-    PS_ASSERT (readout, false);
-    PS_ASSERT (readout->parent, false);
-    PS_ASSERT (readout->parent->concepts, false);
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+    psAssert (readout->parent, "missing cell?");
+    psAssert (readout->parent->concepts, "missing concepts?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
 
     psTimerStart ("psphot.noise");
@@ -24,5 +57,5 @@
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
-    assert (maskVal);
+    psAssert (maskVal, "missing mask value?");
 
     // increase variance by factor*(object noise):
@@ -42,5 +75,5 @@
     PS_ASSERT (status, false);
     if (isfinite(GAIN)) {
-	FACTOR /= GAIN;
+        FACTOR /= GAIN;
     }
 
@@ -50,4 +83,5 @@
 
         // skip sources which were not subtracted
+	// NOTE: this bit is not modified when pmSourceOp applies to noise
         if (!(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED)) continue;
 
@@ -70,7 +104,7 @@
         oldshape.sxy = PAR[PM_PAR_SXY];
 
-	// XXX can this be done more intelligently?
-	if (oldI0 == 0.0) continue;
-	if (!isfinite(oldI0)) continue;
+        // XXX can this be done more intelligently?
+        if (oldI0 == 0.0) continue;
+        if (!isfinite(oldI0)) continue;
 
         // increase size and height of source
@@ -79,5 +113,5 @@
         axes.minor *= SIZE;
         newshape = psEllipseAxesToShape (axes);
-        PAR[PM_PAR_I0]  = FACTOR*PS_SQR(oldI0);
+        PAR[PM_PAR_I0]  = FACTOR*oldI0;
         PAR[PM_PAR_SXX] = newshape.sx;
         PAR[PM_PAR_SYY] = newshape.sy;
Index: branches/simtest_nebulous_branches/psphot/src/psphotApResid.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotApResid.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotApResid.c	(revision 27840)
@@ -4,5 +4,32 @@
 // measure the aperture residual statistics and 2D variations
 
-bool psphotApResid (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf)
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotApResid (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+	if (!psphotApResidReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to measure aperture residual for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotApResidReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe)
 {
     int Nfail = 0;
@@ -13,14 +40,26 @@
     pmSource *source;
 
-    PS_ASSERT_PTR_NON_NULL(config, false);
-    PS_ASSERT_PTR_NON_NULL(readout, false);
-    PS_ASSERT_PTR_NON_NULL(sources, false);
-    PS_ASSERT_PTR_NON_NULL(psf, false);
-
     psTimerStart ("psphot.apresid");
 
-    // select the appropriate recipe information
-    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
-    assert (recipe);
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping ap resid");
+	return true;
+    }
+
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    psAssert (psf, "missing psf?");
 
     // determine the number of allowed threads
@@ -33,10 +72,8 @@
     if (!measureAptrend) {
         // save nan values since these were not calculated
-        psMetadataAdd (recipe, PS_LIST_TAIL, "SKYBIAS",  PS_DATA_F32 | PS_META_REPLACE, "aperture sky bias",   NAN);
-        psMetadataAdd (recipe, PS_LIST_TAIL, "SKYSAT",   PS_DATA_F32 | PS_META_REPLACE, "aperture-determined saturation",   NAN);
-        psMetadataAdd (recipe, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   NAN);
-        psMetadataAdd (recipe, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", NAN);
-        psMetadataAdd (recipe, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", NAN);
-        psMetadataAdd (recipe, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", 0);
+        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   NAN);
+        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", NAN);
+        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", 0);
+        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", NAN);
         return true;
     }
@@ -53,8 +90,10 @@
     maskVal |= markVal;
 
-    // S/N limit to perform full non-linear fits
+    // clipping for extreme outliers
+    // XXX this is not currently defined in the recipe
     float MAX_AP_OFFSET = psMetadataLookupF32 (&status, recipe, "MAX_AP_OFFSET");
 
-    // this is the smallest radius allowed: need to at least extend growth curve down to this...
+    // options for how the photometry is calculated
+    // XXX are these sensible?
     bool IGNORE_GROWTH = psMetadataLookupBool (&status, recipe, "IGNORE_GROWTH");
     bool INTERPOLATE_AP = psMetadataLookupBool (&status, recipe, "INTERPOLATE_AP");
@@ -100,4 +139,5 @@
 	    PS_ARRAY_ADD_SCALAR(job->args, photMode, PS_TYPE_S32);
 	    PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
+	    PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
 
 	    PS_ARRAY_ADD_SCALAR(job->args, 0,        PS_TYPE_S32); // this is used as a return value for Nskip
@@ -110,17 +150,4 @@
 	    }
 	    psFree(job);
-
-# if (0)
-		int nskip = 0;
-		int nfail = 0;
-
-		if (!psphotApResidMags_Unthreaded (&nskip, &nfail, sources, psf, photMode, maskVal)) {
-		    psError(PS_ERR_UNKNOWN, false, "Unable to guess model.");
-		    return false;
-		}
-		Nskip += nskip;
-		Nfail += nfail;
-# endif
-
 	}
 
@@ -138,7 +165,7 @@
 	    } else {
 		psScalar *scalar = NULL;
-		scalar = job->args->data[4];
+		scalar = job->args->data[5];
 		Nskip += scalar->data.S32;
-		scalar = job->args->data[5];
+		scalar = job->args->data[6];
 		Nfail += scalar->data.S32;
 	    }
@@ -150,5 +177,4 @@
 
     // gather the stats to assess the aperture residuals
-    psVector *mask    = psVectorAllocEmpty (300, PS_TYPE_VECTOR_MASK);
     psVector *mag     = psVectorAllocEmpty (300, PS_TYPE_F32);
     psVector *xPos    = psVectorAllocEmpty (300, PS_TYPE_F32);
@@ -158,4 +184,9 @@
     Npsf = 0;
 
+# ifdef DEBUG    
+    FILE *f = fopen ("apresid.dat", "w");
+    psAssert (f, "failed open");
+# endif
+
     for (int i = 0; i < sources->n; i++) {
         source = sources->data[i];
@@ -170,8 +201,12 @@
 	if (source->mode &  PM_SOURCE_MODE_EXT_LIMIT) SKIPSTAR ("EXTENDED");
 	if (source->mode &  PM_SOURCE_MODE_CR_LIMIT) SKIPSTAR ("COSMIC RAY");
+	if (source->mode &  PM_SOURCE_MODE_DEFECT) SKIPSTAR ("DEFECT");
 	    
         if (!isfinite(source->apMag) || !isfinite(source->psfMag)) {
             continue;
         }
+
+	// XXX make this user-configurable?
+	if (source->errMag > 0.01) continue;
 
         // aperture residual for this source
@@ -185,19 +220,24 @@
         }
 
-        mag->data.F32[Npsf]     = source->psfMag;
-        apResid->data.F32[Npsf] = dap;
-        xPos->data.F32[Npsf]    = model->params->data.F32[PM_PAR_XPOS];
-        yPos->data.F32[Npsf]    = model->params->data.F32[PM_PAR_YPOS];
-
-        mask->data.PS_TYPE_VECTOR_MASK_DATA[Npsf] = 0;
-
-        dMag->data.F32[Npsf] = model->dparams->data.F32[PM_PAR_I0] / model->params->data.F32[PM_PAR_I0];
-
-        psVectorExtend (mag,     100, 1);
-        psVectorExtend (mask,    100, 1);
-        psVectorExtend (xPos,    100, 1);
-        psVectorExtend (yPos,    100, 1);
-        psVectorExtend (dMag,    100, 1);
-        psVectorExtend (apResid, 100, 1);
+# ifdef DEBUG
+	fprintf (f, "%6.1f %6.1f : %6.1f %6.1f : %8.3f %8.3f %8.3f : %f : %f %f %f : %f\n",
+		 source->peak->xf, source->peak->yf, 
+		 source->modelPSF->params->data.F32[PM_PAR_XPOS], source->modelPSF->params->data.F32[PM_PAR_YPOS], 
+		 source->psfMag, source->apMag, source->errMag,
+		 source->modelPSF->params->data.F32[PM_PAR_I0], 
+		 source->modelPSF->params->data.F32[PM_PAR_SXX], source->modelPSF->params->data.F32[PM_PAR_SXY], source->modelPSF->params->data.F32[PM_PAR_SYY], 
+		 source->modelPSF->params->data.F32[PM_PAR_7]);
+# endif
+	if (!isfinite(source->psfMag)) psAbort ("nan in psfMag");
+	if (!isfinite(source->errMag)) psAbort ("nan in errMag");
+	if (!isfinite(source->apMag)) psAbort ("nan in apMag");
+	if (!isfinite(model->params->data.F32[PM_PAR_XPOS])) psAbort ("nan in xPos");
+	if (!isfinite(model->params->data.F32[PM_PAR_YPOS])) psAbort ("nan in yPos");
+
+        psVectorAppend (mag, source->psfMag);
+        psVectorAppend (dMag,source->errMag);
+        psVectorAppend (apResid, dap);
+        psVectorAppend (xPos, model->params->data.F32[PM_PAR_XPOS]);
+        psVectorAppend (yPos, model->params->data.F32[PM_PAR_YPOS]);
         Npsf ++;
     }
@@ -205,4 +245,8 @@
     psLogMsg ("psphot.apresid", PS_LOG_DETAIL, "measure aperture residuals for %d objects (%d skipped, %d failed, %ld invalid)\n",
               Npsf, Nskip, Nfail, sources->n - Npsf - Nskip - Nfail);
+
+# ifdef DEBUG
+    fclose (f);
+# endif
 
     // XXX choose a better value here?
@@ -212,47 +256,163 @@
     }
 
-    // XXX deprecating the old code which allowed the ApResid to be fitted as a function of flux and r^2/flux
-    // XXX is this asymmetric clipping still needed?  this analysis should come after neighbors are subtracted...
-    // 3hi/1lo sigma clipping on the rflux vs metric fit
-    // systematic error information
-    float errorScale = 0.0;
-    float errorFloor = 0.0;
-
+    // this is a bit tricky, because we have two cases (MAP vs POLY), and they have a different 
+    // definition for 'order' (order_MAP = order_POLY + 1).  in addition, we have a 
+    // user-specified MAX order, which we should respect, regardless of the mode
+
+    // set the max order (0 = constant) which the number of psf stars can support:
+    // rule of thumb: require 3 stars per 'cell' (order+1)^2
+    int MaxOrderForStars = 0;
+    if (Npsf >= 12) MaxOrderForStars = 1; // 4 cells
+    if (Npsf >= 27) MaxOrderForStars = 2; // 9 cells
+    if (Npsf >= 48) MaxOrderForStars = 3; // 16 cells
+    if (Npsf >  75) MaxOrderForStars = 4; // 25 cells
+
+    pmTrend2DMode mode = PM_TREND_MAP;
+    if (mode == PM_TREND_MAP) {
+	MaxOrderForStars ++;
+    } 
+    APTREND_ORDER_MAX = PS_MIN (APTREND_ORDER_MAX, MaxOrderForStars);
+
+    psFree (psf->ApTrend);
+    psf->ApTrend = NULL;
     float errorFloorMin = FLT_MAX;
-    int entryMin = -1;
-
-    // Fit out the dap vs mag trend, iterate over spatial scale until error Floor increases.
-    // Stop if Npsf / (Nx * Ny) < 3
+
+    // as we loop over orders, we need to refer to the initial selection, but we modify the
+    // option values to match the current guess: save the max values here:
+    int NX = readout->image->numCols;
+    int NY = readout->image->numRows;
     for (int i = 1; i <= APTREND_ORDER_MAX; i++) {
-
-        if (!psphotApResidTrend (readout, psf, Npsf, i, &errorScale, &errorFloor, mask, xPos, yPos, apResid, dMag)) {
-            break;
-        }
-
-        // store the resulting errorFloor values and the scales, redo the best
+	
+	int Nx, Ny;
+	if (NX > NY) {
+	    Nx = i;
+	    Ny = PS_MAX (1, (int)(i * (NY / (float)(NX)) + 0.5));
+	} else {
+	    Ny = i;
+	    Nx = PS_MAX (1, (int)(i * (NX / (float)(NY)) + 0.5));
+	}
+
+	float errorFloor;
+        pmTrend2D *apTrend = psphotApResidTrend (&errorFloor, readout, Nx, Ny, xPos, yPos, apResid, dMag);
+	if (!apTrend) {
+	    continue;
+	}
+
+	// apply ApTrend results
+	// float xc = 0.5*readout->image->numCols + readout->image->col0 + 0.5;
+	// float yc = 0.5*readout->image->numRows + readout->image->row0 + 0.5;
+	// float ApResid = pmTrend2DEval (psf->ApTrend, xc, yc); // ap-fit at chip center
+	// if (!isfinite(ApResid)) psAbort("nan apresid @ center");
+
+        // store the minimum errorFloor and best ApTrend to keep
         if (errorFloor < errorFloorMin) {
             errorFloorMin = errorFloor;
-            entryMin = i;
+	    psFree (psf->ApTrend);
+	    psf->ApTrend = psMemIncrRefCounter(apTrend);
         }
-    }
-    if (entryMin == -1) {
+	psFree (apTrend);
+    }
+    if (psf->ApTrend == NULL) {
         psWarning("Failed to find a valid aperture residual value");
 	goto escape;
     }
 
-    // XXX catch error condition
-    psphotApResidTrend (readout, psf, Npsf, entryMin, &errorScale, &errorFloor, mask, xPos, yPos, apResid, dMag);
+    // apply ApTrend results
+    float xc = 0.5*readout->image->numCols + readout->image->col0 + 0.5;
+    float yc = 0.5*readout->image->numRows + readout->image->row0 + 0.5;
+
+    psf->ApResid  = pmTrend2DEval (psf->ApTrend, xc, yc); // ap-fit at chip center
+    psf->dApResid = errorFloorMin;
+    psf->nApResid = Npsf;
+
+    // save results for later output
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   psf->ApResid);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", psf->dApResid);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", psf->nApResid);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", psf->growth->apLoss);
+
+    psLogMsg ("psphot.apresid", PS_LOG_DETAIL, "aperture residual: %f +/- %f\n", psf->ApResid, psf->dApResid);
+    psLogMsg ("psphot.apresid", PS_LOG_INFO, "measure full-frame aperture residuals for %d sources: %f sec\n", Npsf, psTimerMark ("psphot.apresid"));
+
+    psFree (xPos);
+    psFree (yPos);
+    psFree (apResid);
+    psFree (mag);
+    psFree (dMag);
+
+    psphotVisualPlotApResid (sources, psf->ApResid, psf->dApResid);
+
+    return true;
+
+escape:
+    // save nan values since these were not calculated
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   NAN);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", NAN);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", 0);
+    psMetadataAdd (readout->analysis, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", NAN);
+
+    psFree (xPos);
+    psFree (yPos);
+    psFree (apResid);
+    psFree (mag);
+    psFree (dMag);
+    return true;
+    // this is a quality error, not a programming error
+}
+
+pmTrend2D *psphotApResidTrend (float *apResidSysErr, pmReadout *readout, int Nx, int Ny, psVector *xPos, psVector *yPos, psVector *apResid, psVector *dMag) {
+
+    // the mask marks the values not used to calculate the ApTrend
+    psVector *mask = psVectorAlloc(xPos->n, PS_TYPE_VECTOR_MASK);
+    psVectorInit (mask, 0);
+
+    // XXX allow user to set this optionally?
+    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+
+    // measure Trend2D for the current spatial scale
+    pmTrend2D *apTrend = pmTrend2DAlloc (PM_TREND_MAP, readout->image, Nx, Ny, stats);
+
+    // XXX somewhat arbitrary: soften the errors so the few bright stars do not totally dominate:
+    // XXX use this or not?  probably not, since this is the point of the systematic error analysis
+    psVector *dMagSoft = psVectorAlloc (dMag->n, PS_TYPE_F32);
+    for (int i = 0; i < dMag->n; i++) {
+        dMagSoft->data.F32[i] = hypot(dMag->data.F32[i], 0.005);
+    }
+
+    // XXX test for errors here
+    if (!pmTrend2DFit (apTrend, mask, 0xff, xPos, yPos, apResid, dMagSoft)) {
+        psWarning("Failed to fit trend for %d x %d map", Nx, Ny);
+	psFree (apTrend);
+	return NULL;
+    }
+    if (apTrend->mode == PM_TREND_MAP) {
+	// p_psImagePrint (2, apTrend->map->map, "ApTrend Before"); // XXX TEST:
+	psImageMapRepair (apTrend->map->map);
+	// p_psImagePrint (2, apTrend->map->map, "ApTrend After"); // XXX TEST:
+    }
 
     // construct the fitted values and the residuals
-    psVector *apResidFit = pmTrend2DEvalVector (psf->ApTrend, mask, 0xff, xPos, yPos);
+    psVector *apResidFit = pmTrend2DEvalVector (apTrend, mask, 0xff, xPos, yPos);
     psVector *apResidRes = (psVector *) psBinaryOp (NULL, (void *) apResid, "-", (void *) apResidFit);
-    psVector *dMagSys = (psVector *) psBinaryOp (NULL, (void *) dMag, "*", (void *) psScalarAlloc(errorScale, PS_TYPE_F32));
-
-    if (psTraceGetLevel("psphot") >= 2) {
-        FILE *dumpFile = fopen ("apresid.dat", "w");
+
+    // measure systematic error
+    *apResidSysErr = psVectorSystematicError (apResidRes, dMag, 0.10);
+    if (!isfinite(*apResidSysErr)) {
+        psWarning("Failed to find systematic error for %d x %d map", Nx, Ny);
+	psFree (apTrend);
+	return NULL;
+    }
+
+    psLogMsg ("psphot.apresid", PS_LOG_INFO, "result of %d x %d grid\n", Nx, Ny);
+    psLogMsg ("psphot.apresid", PS_LOG_INFO, "systematic scatter floor: %f\n", *apResidSysErr);
+
+    if (psTraceGetLevel("psphot") >= 4) {
+	char filename[64];
+	snprintf (filename, 64, "apresid.%dx%d.dat", Nx, Ny);
+        FILE *dumpFile = fopen (filename, "w");
         for (int i = 0; i < xPos->n; i++) {
-            fprintf (dumpFile, "%f %f  %f %f %f  %f %f  %x\n",
+            fprintf (dumpFile, "%f %f  %f %f  %f %f  %x\n",
                      xPos->data.F32[i], yPos->data.F32[i],
-                     mag->data.F32[i], dMag->data.F32[i], dMagSys->data.F32[i],
+                     dMag->data.F32[i], hypot(dMag->data.F32[i], *apResidSysErr),
                      apResid->data.F32[i], apResidRes->data.F32[i],
                      mask->data.PS_TYPE_VECTOR_MASK_DATA[i]);
@@ -261,204 +421,5 @@
     }
 
-    // apply ApTrend results
-    float xc = 0.5*readout->image->numCols + readout->image->col0 + 0.5;
-    float yc = 0.5*readout->image->numRows + readout->image->row0 + 0.5;
-
-    psf->ApResid  = pmTrend2DEval (psf->ApTrend, xc, yc); // ap-fit at chip center
-    psf->dApResid = errorFloor;
-    psf->nApResid = Npsf;
-
-    // save results for later output
-    psMetadataAdd (recipe, PS_LIST_TAIL, "SKYBIAS",  PS_DATA_F32 | PS_META_REPLACE, "aperture sky bias",   0.0);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "SKYSAT",   PS_DATA_F32 | PS_META_REPLACE, "aperture-determined saturation",   0.0);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   psf->ApResid);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", psf->dApResid);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", psf->growth->apLoss);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", psf->nApResid);
-
-    psLogMsg ("psphot.apresid", PS_LOG_DETAIL, "aperture residual: %f +/- %f\n", psf->ApResid, psf->dApResid);
-    psLogMsg ("psphot.apresid", PS_LOG_INFO, "measure full-frame aperture residuals for %d sources: %f sec\n", Npsf, psTimerMark ("psphot.apresid"));
-
-    psFree (mag);
     psFree (mask);
-    psFree (xPos);
-    psFree (yPos);
-
-    psFree (apResid);
-    psFree (apResidFit);
-    psFree (apResidRes);
-
-    psFree (dMagSys);
-    psFree (dMag);
-
-    psphotVisualPlotApResid (sources);
-
-    return true;
-
-escape:
-    // save nan values since these were not calculated
-    psMetadataAdd (recipe, PS_LIST_TAIL, "SKYBIAS",  PS_DATA_F32 | PS_META_REPLACE, "aperture sky bias",   NAN);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "SKYSAT",   PS_DATA_F32 | PS_META_REPLACE, "aperture-determined saturation",   NAN);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "APMIFIT",  PS_DATA_F32 | PS_META_REPLACE, "aperture residual",   NAN);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "DAPMIFIT", PS_DATA_F32 | PS_META_REPLACE, "ap residual scatter", NAN);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "APLOSS",   PS_DATA_F32 | PS_META_REPLACE, "aperture loss (mag)", NAN);
-    psMetadataAdd (recipe, PS_LIST_TAIL, "NAPMIFIT", PS_DATA_S32 | PS_META_REPLACE, "number of apresid stars", 0);
-
-    psFree (mag);
-    psFree (mask);
-    psFree (xPos);
-    psFree (yPos);
-    psFree (apResid);
-    psFree (dMag);
-    return false;
-}
-
-/*
-  (aprMag' - fitMag) = rflux*skyBias + ApTrend(x,y)
-  (aprMag - rflux*skyBias) - fitMag = ApTrend(x,y)
-  (aprMag - rflux*skyBias) = fitMag + ApTrend(x,y)
-*/
-
-// XXX this still sucks...  need a better way to estimate the error floor...
-bool psphotMagErrorScale (float *errorScale, float *errorFloor, psVector *dMag, psVector *dap, psVector *mask, int nGroup) {
-
-    psStats *statsS = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
-    psStats *statsM = psStatsAlloc (PS_STAT_SAMPLE_MEAN);
-
-    // measure the trend in bins with 10 values each; if < 10 total, use them all
-    int nBin = PS_MAX (dMag->n / nGroup, 1);
-
-    // output vectors for ApResid trend
-    psVector *dSo = psVectorAlloc (nBin, PS_TYPE_F32);
-    psVector *dMo = psVectorAlloc (nBin, PS_TYPE_F32);
-    psVector *dRo = psVectorAlloc (nBin, PS_TYPE_F32);
-
-    // use dMag to group the dMag and dap vectors
-    psVector *index = psVectorSortIndex (NULL, dMag);
-
-    // subset vectors for dMag and dap values within the given range
-    psVector *dMSubset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
-    psVector *dASubset = psVectorAllocEmpty (nGroup, PS_TYPE_F32);
-    psVector *mkSubset = psVectorAllocEmpty (nGroup, PS_TYPE_VECTOR_MASK);
-
-    int n = 0;
-    for (int i = 0; i < dMo->n; i++) {
-        int j;
-        for (j = 0; (j < nGroup) && (n < dMag->n); j++, n++) {
-            int N = index->data.U32[n];
-            dMSubset->data.F32[j] = dMag->data.F32[N];
-            dASubset->data.F32[j] = dap->data.F32[N];
-            mkSubset->data.PS_TYPE_VECTOR_MASK_DATA[j] = mask->data.PS_TYPE_VECTOR_MASK_DATA[N];
-        }
-        dMSubset->n = j;
-        dASubset->n = j;
-        mkSubset->n = j;
-
-        psStatsInit (statsS);
-        psStatsInit (statsM);
-
-        if (j > 2) {
-            if (!psVectorStats (statsS, dASubset, NULL, mkSubset, 0xff)) {
-		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
-		return false;
-	    }
-            if (!psVectorStats (statsM, dMSubset, NULL, mkSubset, 0xff)) {
-		psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
-		return false;
-	    }
-            dSo->data.F32[i] = statsS->robustStdev;
-            dMo->data.F32[i] = statsM->sampleMean;
-            dRo->data.F32[i] = statsS->robustStdev / statsM->sampleMean;
-        } else {
-            dSo->data.F32[i] = NAN;
-            dMo->data.F32[i] = NAN;
-            dRo->data.F32[i] = NAN;
-        }
-    }
-    psFree (dMSubset);
-    psFree (dASubset);
-    psFree (mkSubset);
-
-    psStats *stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN);
-    if (!psVectorStats (stats, dRo, NULL, NULL, 0)) {
-	psError(PS_ERR_UNKNOWN, false, "failure to measure stats");
-	return false;
-    }
-
-    *errorScale = stats->sampleMedian;
-    for (int i = 0; i < dSo->n; i++) {
-        *errorFloor = dSo->data.F32[i];
-        if (fabs(*errorFloor) <= FLT_EPSILON) continue;
-        if (isfinite(*errorFloor)) break;
-    }
-
-    psFree (stats);
-    psFree (index);
-
-    psFree (dRo);
-    psFree (dMo);
-    psFree (dSo);
-
-    psFree (statsS);
-    psFree (statsM);
-
-    return true;
-}
-
-bool psphotApResidTrend (pmReadout *readout, pmPSF *psf, int Npsf, int scale, float *errorScale, float *errorFloor, psVector *mask, psVector *xPos, psVector *yPos, psVector *apResid, psVector *dMag) {
-
-    int Nx, Ny;
-
-    if (readout->image->numCols > readout->image->numRows) {
-        Nx = scale;
-        float AR = readout->image->numRows / (float) readout->image->numCols;
-        Ny = (int) (Nx * AR + 0.5);
-        Ny = PS_MAX (1, Ny);
-    } else {
-        Ny = scale;
-        float AR = readout->image->numRows / (float) readout->image->numCols;
-        Nx = (int) (Ny * AR + 0.5);
-        Nx = PS_MAX (1, Nx);
-    }
-
-    // require at least 10 stars per spatial bin
-    if (Npsf < 10*Nx*Ny) {
-        return false;
-    }
-
-    // the mask marks the values not used to calculate the ApTrend
-    psVectorInit (mask, 0);
-
-    // XXX stats structure for use by ApTrend : make parameters user setable
-    psStats *stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
-    stats->min = 2.0;
-    stats->max = 3.0;
-
-    // measure Trend2D for the current spatial scale
-    psFree (psf->ApTrend);
-    psf->ApTrend = pmTrend2DAlloc (PM_TREND_MAP, readout->image, Nx, Ny, stats);
-
-    // XXX somewhat arbitrary: soften the errors so the few bright stars do not totally dominate:
-    psVector *dMagSoft = psVectorAlloc (dMag->n, PS_TYPE_F32);
-    for (int i = 0; i < dMag->n; i++) {
-        dMagSoft->data.F32[i] = hypot(dMag->data.F32[i], 0.01);
-    }
-
-    // XXX test for errors here
-    pmTrend2DFit (psf->ApTrend, mask, 0xff, xPos, yPos, apResid, dMagSoft);
-
-    // construct the fitted values and the residuals
-    psVector *apResidFit = pmTrend2DEvalVector (psf->ApTrend, mask, 0xff, xPos, yPos);
-    psVector *apResidRes = (psVector *) psBinaryOp (NULL, (void *) apResid, "-", (void *) apResidFit);
-
-    // 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);
-    psphotMagErrorScale (errorScale, errorFloor, dMag, apResidRes, mask, nGroup);
-
-    psLogMsg ("psphot.apresid", PS_LOG_INFO, "result of %d x %d grid (%d stars per bin)\n", Nx, Ny, nGroup);
-    psLogMsg ("psphot.apresid", PS_LOG_INFO, "systematic error / photon error: %f\n", *errorScale);
-    psLogMsg ("psphot.apresid", PS_LOG_INFO, "systematic scatter floor: %f\n", *errorFloor);
-
     psFree (stats);
     psFree (dMagSoft);
@@ -466,5 +427,5 @@
     psFree (apResidRes);
 
-    return true;
+    return apTrend;
 }
 
@@ -480,4 +441,5 @@
     pmSourcePhotometryMode photMode = PS_SCALAR_VALUE(job->args->data[2],S32);
     psImageMaskType maskVal         = PS_SCALAR_VALUE(job->args->data[3],PS_TYPE_IMAGE_MASK_DATA);
+    psImageMaskType markVal         = PS_SCALAR_VALUE(job->args->data[4],PS_TYPE_IMAGE_MASK_DATA);
 
     for (int i = 0; i < sources->n; i++) {
@@ -490,61 +452,42 @@
         if (source->mode &  PM_SOURCE_MODE_POOR) SKIPSTAR ("POOR STAR");
 
-        if (!pmSourceMagnitudes (source, psf, photMode, maskVal)) {
-            Nskip ++;
-            psTrace ("psphot", 3, "skip : bad source mag");
-            continue;
+        // replace object in image
+        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
         }
 
-        if (!isfinite(source->apMag) || !isfinite(source->psfMag)) {
-            Nfail ++;
-            psTrace ("psphot", 3, "fail : nan mags : %f %f", source->apMag, source->psfMag);
-            continue;
-        }
-        source->mode |= PM_SOURCE_MODE_AP_MAGS;
+	// clear the mask bit and set the circular mask pixels
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal));
+	psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, source->apRadius, "OR", markVal);
+
+	bool status = pmSourceMagnitudes (source, psf, photMode, maskVal);
+
+	// clear the mask bit 
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal));
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+
+	if (!status) {
+	    Nskip ++;
+	    psTrace ("psphot", 3, "skip : bad source mag");
+	    continue;
+	}
+    
+	if (!isfinite(source->apMag) || !isfinite(source->psfMag)) {
+	    Nfail ++;
+	    psTrace ("psphot", 3, "fail : nan mags : %f %f", source->apMag, source->psfMag);
+	    continue;
+	}
+	source->mode |= PM_SOURCE_MODE_AP_MAGS;
     }
 
     // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    scalar = job->args->data[4];
+    scalar = job->args->data[5];
     scalar->data.S32 = Nskip;
 
-    scalar = job->args->data[5];
+    scalar = job->args->data[6];
     scalar->data.S32 = Nfail;
 
     return true;
 }
-
-# if (0)
-bool psphotApResidMags_Unthreaded (int *nskip, int *nfail, psArray *sources, pmPSF *psf, pmSourcePhotometryMode photMode, psImageMaskType maskVal) {
-
-    int Nskip = 0;
-    int Nfail = 0;
-
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = (pmSource *) sources->data[i];
-
-        if (source->type != PM_SOURCE_TYPE_STAR) SKIPSTAR ("NOT STAR");
-        if (source->mode &  PM_SOURCE_MODE_SATSTAR) SKIPSTAR ("SATSTAR");
-        if (source->mode &  PM_SOURCE_MODE_BLEND) SKIPSTAR ("BLEND");
-        if (source->mode &  PM_SOURCE_MODE_FAIL) SKIPSTAR ("FAIL STAR");
-        if (source->mode &  PM_SOURCE_MODE_POOR) SKIPSTAR ("POOR STAR");
-
-        if (!pmSourceMagnitudes (source, psf, photMode, maskVal)) {
-            Nskip ++;
-            psTrace ("psphot", 3, "skip : bad source mag");
-            continue;
-        }
-
-        if (!isfinite(source->apMag) || !isfinite(source->psfMag)) {
-            Nfail ++;
-            psTrace ("psphot", 3, "fail : nan mags : %f %f", source->apMag, source->psfMag);
-            continue;
-        }
-    }
-
-    // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    *nskip = Nskip;
-    *nfail = Nfail;
-
-    return true;
-}
-# endif
Index: branches/simtest_nebulous_branches/psphot/src/psphotArguments.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotArguments.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotArguments.c	(revision 27840)
@@ -218,8 +218,8 @@
     // override any configuration-specified source for these files
     //
-    pmConfigFileSetsMD (config->arguments, &argc, argv, "MASK",   "-mask",   "-masklist");
-    pmConfigFileSetsMD (config->arguments, &argc, argv, "VARIANCE", "-variance", "-variancelist");
-    pmConfigFileSetsMD (config->arguments, &argc, argv, "PSPHOT.PSF", "-psf", "-psflist");
-    pmConfigFileSetsMD (config->arguments, &argc, argv, "SRC",    "-src",    "-srclist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "MASK",       "-mask",     "-masklist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "VARIANCE",   "-variance", "-variancelist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "PSPHOT.PSF", "-psf",      "-psflist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "SRC",        "-src",      "-srclist");
 
     if (argc == 1) {
@@ -228,6 +228,6 @@
     }
 
-    if (psArgumentGet(argc, argv, "-help")
-	|| psArgumentGet(argc, argv, "-h"))
+    if (psArgumentGet(argc, argv, "-help") ||
+	psArgumentGet(argc, argv, "-h"))
       writeHelpInfo(argv[0], config, stdout);
       
Index: branches/simtest_nebulous_branches/psphot/src/psphotBasicDeblend.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotBasicDeblend.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotBasicDeblend.c	(revision 27840)
@@ -1,5 +1,23 @@
 # include "psphotInternal.h"
 
-bool psphotBasicDeblend (psArray *sources, psMetadata *recipe) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotBasicDeblend (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotBasicDeblendReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on basic deblend analysis for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotBasicDeblendReadout (pmConfig *config, const pmFPAview *view, const char *filename, int fileIndex) {
 
     int N;
@@ -8,7 +26,29 @@
     pmSource *source, *testSource;
 
+    psTimerStart ("psphot.deblend.basic");
+
     int Nblend = 0;
 
-    psTimerStart ("psphot.deblend.basic");
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, fileIndex); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping basic deblend");
+	return true;
+    }
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
 
     float FRACTION = psMetadataLookupF32 (&status, recipe, "DEBLEND_PEAK_FRACTION");
Index: branches/simtest_nebulous_branches/psphot/src/psphotBlendFit.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotBlendFit.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotBlendFit.c	(revision 27840)
@@ -1,6 +1,28 @@
 # include "psphotInternal.h"
 
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotBlendFit (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotBlendFitReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to fit sources (non-linear) for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // XXX I don't like this name
-bool psphotBlendFit (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf) {
+bool psphotBlendFitReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     int Nfit = 0;
@@ -12,7 +34,24 @@
     psTimerStart ("psphot.fit.nonlinear");
 
-    // select the appropriate recipe information
-    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
-    assert (recipe);
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping blend fit");
+	return true;
+    }
+
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    psAssert (psf, "missing psf?");
 
     // determine the number of allowed threads
@@ -40,5 +79,5 @@
     psphotInitLimitsPSF (recipe, readout);
     psphotInitLimitsEXT (recipe);
-    psphotInitRadiusPSF (recipe, psf->type);
+    psphotInitRadiusPSF (recipe, readout->analysis, psf->type);
 
     // starts the timer, sets up the array of fitSets
@@ -47,4 +86,8 @@
     // source analysis is done in S/N order (brightest first)
     sources = psArraySort (sources, pmSourceSortBySN);
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping blend");
+	return true;
+    }
 
     // choose Cx, Cy (see psphotThreadTools.c for overview of the concepts)
@@ -254,5 +297,4 @@
         pmSourceCacheModel (source, maskVal);
         pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
     }
 
@@ -273,106 +315,2 @@
 }
 
-# if (0)
-bool psphotBlendFit_Unthreaded (int *nfit, int *npsf, int *next, int *nfail, pmReadout *readout, psMetadata *recipe, psArray *sources, pmPSF *psf, psArray *newSources) {
-
-    bool status = false;
-    int Nfit = 0;
-    int Npsf = 0;
-    int Next = 0;
-    int Nfail = 0;
-
-    // bit-masks to test for good/bad pixels
-    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT");
-    assert (maskVal);
-
-    // bit-mask to mark pixels not used in analysis
-    psImageMaskType markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT");
-    assert (markVal);
-
-    // maskVal is used to test for rejected pixels, and must include markVal
-    maskVal |= markVal;
-
-    // S/N limit to perform full non-linear fits
-    float FIT_SN_LIM = psMetadataLookupF32 (&status, recipe, "FULL_FIT_SN_LIM");
-
-    // option to limit analysis to a specific region
-    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
-    psRegion AnalysisRegion = psRegionForImage (readout->image, psRegionFromString (region));
-    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
-
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-
-        // skip non-astronomical objects (very likely defects)
-        if (source->mode &  PM_SOURCE_MODE_BLEND) continue;
-        if (source->mode &  PM_SOURCE_MODE_CR_LIMIT) continue;
-        if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
-        if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
-
-        // skip DBL second sources (ie, added by psphotFitBlob)
-        if (source->mode &  PM_SOURCE_MODE_PAIR) continue;
-
-        // limit selection to some SN limit
-        if (source->peak->SN < FIT_SN_LIM) continue;
-
-        // exclude sources outside optional analysis region
-        if (source->peak->xf < AnalysisRegion.x0) continue;
-        if (source->peak->yf < AnalysisRegion.y0) continue;
-        if (source->peak->xf > AnalysisRegion.x1) continue;
-        if (source->peak->yf > AnalysisRegion.y1) continue;
-
-        // if model is NULL, we don't have a starting guess
-        if (source->modelPSF == NULL) continue;
-
-        // skip sources which are insignificant flux?
-        // XXX this is somewhat ad-hoc
-        if (source->modelPSF->params->data.F32[1] < 0.1) {
-            psTrace ("psphot", 5, "skipping near-zero source: %f, %f : %f\n",
-                     source->modelPSF->params->data.F32[1],
-                     source->modelPSF->params->data.F32[2],
-                     source->modelPSF->params->data.F32[3]);
-            continue;
-        }
-
-        // replace object in image
-        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
-            pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-        }
-        Nfit ++;
-
-        // try fitting PSFs or extended sources depending on source->mode
-        // these functions subtract the resulting fitted source
-        if (source->mode & PM_SOURCE_MODE_EXT_LIMIT) {
-            if (psphotFitBlob (readout, source, newSources, psf, maskVal, markVal)) {
-                source->type = PM_SOURCE_TYPE_EXTENDED;
-                psTrace ("psphot", 5, "source at %7.1f, %7.1f is ext", source->peak->xf, source->peak->yf);
-                Next ++;
-                continue;
-            }
-        } else {
-            if (psphotFitBlend (readout, source, psf, maskVal, markVal)) {
-                source->type = PM_SOURCE_TYPE_STAR;
-                psTrace ("psphot", 5, "source at %7.1f, %7.1f is psf", source->peak->xf, source->peak->yf);
-                Npsf ++;
-                continue;
-            }
-        }
-
-        psTrace ("psphot", 5, "source at %7.1f, %7.1f failed", source->peak->xf, source->peak->yf);
-        Nfail ++;
-
-        // re-subtract the object, leave local sky
-        pmSourceCacheModel (source, maskVal);
-        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
-    }
-
-    // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    *nfit  = Nfit;
-    *npsf  = Npsf;
-    *next  = Next;
-    *nfail = Nfail;
-
-    return true;
-}
-# endif
Index: branches/simtest_nebulous_branches/psphot/src/psphotChoosePSF.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotChoosePSF.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotChoosePSF.c	(revision 27840)
@@ -1,33 +1,69 @@
 # include "psphotInternal.h"
 
-void psphotCountPSFStars (psArray *sources) {
-
-    int nPSF = 0;
-
-    // select the candidate PSF stars (pointers to original sources)
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-        if (source->mode & PM_SOURCE_MODE_PSFSTAR) {
-            nPSF ++;
-        }
-    }
-
-    fprintf (stderr, "N PSF: %d\n", nPSF);
+// generate a PSF model for inputs without PSF models already loaded
+bool psphotChoosePSF (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+        if (!psphotChoosePSFReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to choose a psf model for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
 }
 
 // try PSF models and select best option
-pmPSF *psphotChoosePSF (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+bool psphotChoosePSFReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
 
     psTimerStart ("psphot.choose.psf");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    // do not generate a PSF if we already were supplied one
+    if (psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF")) {
+        psLogMsg ("psphot", PS_LOG_DETAIL, "psf model supplied for input file %d", index);
+        return true;
+    }
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+        psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping PSF model");
+        return false;
+    }
 
     // bit-masks to test for good/bad pixels
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT");
-    assert (maskVal);
+    psAssert (maskVal, "missing mask value?");
 
     // bit-mask to mark pixels not used in analysis
     psImageMaskType markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT");
-    assert (markVal);
+    psAssert (markVal, "missing mark value?");
 
     // maskVal is used to test for rejected pixels, and must include markVal
@@ -73,10 +109,21 @@
     // get the fixed PSF fit radius
     // XXX check that PSF_FIT_RADIUS < SKY_OUTER_RADIUS
-    options->radius = psMetadataLookupF32 (&status, recipe, "PSF_FIT_RADIUS");
-    assert (status);
-
-    // XXX ROBUST seems to be too agressive given the small numbers.
-    // options->stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
-    options->stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    // options->radius = psMetadataLookupF32 (&status, recipe, "PSF_FIT_RADIUS");
+    // assert (status);
+
+    // values on readout->analysis if we have calculated a Gaussian window function, use that
+    // for both the PSF fit radius and the aperture radius (scaling SIGMA), otherwise base the value on the recipe value for MOMENTS_GAUSS_SIGMA
+    float gaussSigma = psMetadataLookupF32 (&status, readout->analysis, "MOMENTS_GAUSS_SIGMA");
+    if (!status) {
+        gaussSigma = psMetadataLookupF32 (&status, recipe, "MOMENTS_GAUSS_SIGMA");
+    }
+    float fitScale = psMetadataLookupF32(&status, recipe, "PSF_FIT_RADIUS_SCALE");
+    float apScale = psMetadataLookupF32(&status, recipe, "PSF_APERTURE_SCALE");
+    options->fitRadius = (int)(fitScale*gaussSigma);
+    options->apRadius = (int)(apScale*gaussSigma);
+
+    // XXX use the same radii for standard analysis as for the PSF creation
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "PSF_FIT_RADIUS", PS_META_REPLACE, "fit radius", options->fitRadius);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "PSF_APERTURE", PS_META_REPLACE, "psf aperture", options->apRadius);
 
     // dimensions of the field for which the PSF is defined
@@ -99,6 +146,4 @@
 
     psArray *stars = psArrayAllocEmpty (sources->n);
-
-    // psphotCountPSFStars (sources);
 
     // select the candidate PSF stars (pointers to original sources)
@@ -115,13 +160,16 @@
     }
 
-    // psphotCountPSFStars (sources);
-
     // check that the identified psf stars sufficiently cover the region; if not, extend the
     // limits somewhat
     psphotCheckStarDistribution (stars, sources, options);
 
-    // psphotCountPSFStars (sources);
-
     psLogMsg ("psphot.pspsf", PS_LOG_DETAIL, "selected candidate %ld PSF objects\n", stars->n);
+
+    if (stars->n < 50) {
+        // ROBUST is too agressive if we only have a small number of PSF stars
+        options->stats = psStatsAlloc (PS_STAT_SAMPLE_MEDIAN | PS_STAT_SAMPLE_STDEV);
+    } else {
+        options->stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+    }
 
     // get the list pointers for the PSF_MODEL entries
@@ -164,5 +212,5 @@
         bool status = true;
         status &= psphotMakeFluxScale (readout->image, recipe, psf);
-        status &= psphotPSFstats (readout, recipe, psf);
+        status &= psphotPSFstats (readout, psf);
         if (!status) {
             psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
@@ -239,5 +287,5 @@
         bool status = true;
         status &= psphotMakeFluxScale (readout->image, recipe, psf);
-        status &= psphotPSFstats (readout, recipe, psf);
+        status &= psphotPSFstats (readout, psf);
         if (!status) {
             psError(PSPHOT_ERR_PSF, false, "Failed to fit any models when choosing PSF");
@@ -289,14 +337,12 @@
     // XXX test dump of psf star data and psf-subtracted image
     if (psTraceGetLevel("psphot.psfstars") > 5) {
-        psphotDumpPSFStars (readout, try, options->radius, maskVal, markVal);
+        psphotDumpPSFStars (readout, try, options->fitRadius, maskVal, markVal);
     }
 
     // save only the best model;
-    // XXX we are not saving the fitted sources
-    // XXX do we want to keep them so we may optionally write them out?
     pmPSF *psf = psMemIncrRefCounter(try->psf);
     psFree (models);
 
-    if (!psphotPSFstats (readout, recipe, psf)) {
+    if (!psphotPSFstats (readout, psf)) {
         psError(PSPHOT_ERR_PSF, false, "cannot measure PSF shape terms");
         psFree(options);
@@ -305,6 +351,4 @@
     }
 
-    // psphotCountPSFStars (sources);
-
     char *modelName = pmModelClassGetName (psf->type);
     psLogMsg ("psphot.pspsf", PS_LOG_INFO, "select psf model: %f sec\n", psTimerMark ("psphot.choose.psf"));
@@ -312,9 +356,20 @@
 
     psFree (options);
-    return (psf);
+
+    // save PSF on readout->analysis
+    if (!psMetadataAddPtr (readout->analysis, PS_LIST_TAIL, "PSPHOT.PSF", PS_META_REPLACE | PS_DATA_UNKNOWN, "psphot psf model", psf)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "problem saving sources on readout");
+        return false;
+    }
+    psFree (psf); // XXX double-check
+
+    // move into psphotChoosePSF
+    psphotVisualShowPSFModel (readout, psf);
+
+    return true;
 }
 
 // measure average parameters of the PSF model
-bool psphotPSFstats (pmReadout *readout, psMetadata *recipe, pmPSF *psf) {
+bool psphotPSFstats (pmReadout *readout, pmPSF *psf) {
 
     psEllipseShape shape;
@@ -322,5 +377,4 @@
 
     PS_ASSERT_PTR_NON_NULL(readout, false);
-    PS_ASSERT_PTR_NON_NULL(recipe, false);
     PS_ASSERT_PTR_NON_NULL(psf, false);
 
@@ -341,5 +395,8 @@
             // create modelPSF from this model
             pmModel *modelPSF = pmModelFromPSFforXY (psf, xc, yc, 1.0);
-            if (!modelPSF) continue;
+            if (!modelPSF) {
+                fprintf (stderr, "?");
+                continue;
+            }
 
             // get the model full-width at half-max
@@ -354,5 +411,8 @@
 
             float FWHM_MINOR = FWHM_MAJOR * (axes.minor / axes.major);
-            if (!isfinite(FWHM_MAJOR) || !isfinite(FWHM_MINOR)) continue;
+            if (!isfinite(FWHM_MAJOR) || !isfinite(FWHM_MINOR)) {
+                fprintf (stderr, "!");
+                continue;
+            }
             psVectorAppend (fwhmMajor, FWHM_MAJOR);
             psVectorAppend (fwhmMinor, FWHM_MINOR);
@@ -366,8 +426,10 @@
     }
 
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_MAJ",   PS_META_REPLACE, "PSF FWHM Major axis (mean)", stats->sampleMean);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_SG",   PS_META_REPLACE, "PSF FWHM Major axis (sigma)", stats->sampleStdev);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_LQ",   PS_META_REPLACE, "PSF FWHM Major axis (lower quartile)", stats->sampleLQ);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_UQ",   PS_META_REPLACE, "PSF FWHM Major axis (upper quartile)", stats->sampleUQ);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FWHM_MAJ",   PS_META_REPLACE, "PSF FWHM Major axis (mean)", stats->sampleMean);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_SG",   PS_META_REPLACE, "PSF FWHM Major axis (sigma)", stats->sampleStdev);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_LQ",   PS_META_REPLACE, "PSF FWHM Major axis (lower quartile)", stats->sampleLQ);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_UQ",   PS_META_REPLACE, "PSF FWHM Major axis (upper quartile)", stats->sampleUQ);
+
+    float fwhmMaj = stats->sampleMean;  // FWHM on major axis
 
     if (!psVectorStats (stats, fwhmMinor, NULL, NULL, 0)) {
@@ -375,12 +437,20 @@
         return false;
     }
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_MIN",   PS_META_REPLACE, "PSF FWHM Minor axis (mean)", stats->sampleMean);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_SG",   PS_META_REPLACE, "PSF FWHM Minor axis (sigma)", stats->sampleStdev);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_LQ",   PS_META_REPLACE, "PSF FWHM Minor axis (lower quartile)", stats->sampleLQ);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_UQ",   PS_META_REPLACE, "PSF FWHM Minor axis (upper quartile)", stats->sampleUQ);
-
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "ANGLE",    PS_META_REPLACE, "PSF angle",           axes.theta);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NPSFSTAR", PS_META_REPLACE, "Number of stars used to make PSF", psf->nPSFstars);
-    psMetadataAddBool(recipe, PS_LIST_TAIL, "PSFMODEL", PS_META_REPLACE, "Valid PSF Model?", true);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FWHM_MIN",   PS_META_REPLACE, "PSF FWHM Minor axis (mean)", stats->sampleMean);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_SG",   PS_META_REPLACE, "PSF FWHM Minor axis (sigma)", stats->sampleStdev);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_LQ",   PS_META_REPLACE, "PSF FWHM Minor axis (lower quartile)", stats->sampleLQ);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_UQ",   PS_META_REPLACE, "PSF FWHM Minor axis (upper quartile)", stats->sampleUQ);
+
+    float fwhmMin = stats->sampleMean;  // FWHM on minor axis
+    if (readout->parent) {
+        pmChip *chip = readout->parent->parent; // Parent chip
+        psAssert(chip, "Cell should be attached to a chip.");
+        psMetadataItem *item = psMetadataLookup(chip->concepts, "CHIP.SEEING"); // Item with chip
+        item->data.F32 = 0.5 * (fwhmMaj + fwhmMin);
+    }
+
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "ANGLE",    PS_META_REPLACE, "PSF angle",           axes.theta);
+    psMetadataAddS32 (readout->analysis, PS_LIST_TAIL, "NPSFSTAR", PS_META_REPLACE, "Number of stars used to make PSF", psf->nPSFstars);
+    psMetadataAddBool(readout->analysis, PS_LIST_TAIL, "PSFMODEL", PS_META_REPLACE, "Valid PSF Model?", true);
 
     psFree (fwhmMajor);
@@ -391,5 +461,5 @@
 
 // determine approximate PSF shape parameters based on the moments
-bool psphotMomentsStats (pmReadout *readout, psMetadata *recipe, psArray *sources) {
+bool psphotMomentsStats (pmReadout *readout, psArray *sources) {
 
     // without the PSF model, we can only rely on the source->moments
@@ -405,5 +475,4 @@
 
     PS_ASSERT_PTR_NON_NULL(readout, false);
-    PS_ASSERT_PTR_NON_NULL(recipe, false);
     PS_ASSERT_PTR_NON_NULL(sources, false);
 
@@ -433,17 +502,22 @@
     FWHM_T /= FWHM_N;
 
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_MAJ",   PS_META_REPLACE, "PSF FWHM Major axis (mean)", FWHM_X);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_SG",   PS_META_REPLACE, "PSF FWHM Major axis (sigma)", 0);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_LQ",   PS_META_REPLACE, "PSF FWHM Major axis (lower quartile)", 0);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MJ_UQ",   PS_META_REPLACE, "PSF FWHM Major axis (upper quartile)", 0);
-
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FWHM_MIN",   PS_META_REPLACE, "PSF FWHM Minor axis (mean)", FWHM_Y);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_SG",   PS_META_REPLACE, "PSF FWHM Minor axis (sigma)", 0);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_LQ",   PS_META_REPLACE, "PSF FWHM Minor axis (lower quartile)", 0);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "FW_MN_UQ",   PS_META_REPLACE, "PSF FWHM Minor axis (upper quartile)", 0);
-
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "ANGLE",    PS_META_REPLACE, "PSF angle",           FWHM_T);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NPSFSTAR", PS_META_REPLACE, "Number of stars used to make PSF", 0);
-    psMetadataAddBool(recipe, PS_LIST_TAIL, "PSFMODEL", PS_META_REPLACE, "Valid PSF Model?", false);
+    if (readout->parent) {
+        pmChip *chip = readout->parent->parent; // Parent chip
+        psAssert(chip, "Cell should be attached to a chip.");
+        psMetadataItem *item = psMetadataLookup(chip->concepts, "CHIP.SEEING"); // Item with chip
+        item->data.F32 = 0.5 * (FWHM_X + FWHM_Y);
+    }
+
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FWHM_MAJ",   PS_META_REPLACE, "PSF FWHM Major axis (mean)", FWHM_X);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_SG",   PS_META_REPLACE, "PSF FWHM Major axis (sigma)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_LQ",   PS_META_REPLACE, "PSF FWHM Major axis (lower quartile)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MJ_UQ",   PS_META_REPLACE, "PSF FWHM Major axis (upper quartile)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FWHM_MIN",   PS_META_REPLACE, "PSF FWHM Minor axis (mean)", FWHM_Y);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_SG",   PS_META_REPLACE, "PSF FWHM Minor axis (sigma)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_LQ",   PS_META_REPLACE, "PSF FWHM Minor axis (lower quartile)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "FW_MN_UQ",   PS_META_REPLACE, "PSF FWHM Minor axis (upper quartile)", 0);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "ANGLE",    PS_META_REPLACE, "PSF angle",           FWHM_T);
+    psMetadataAddS32 (readout->analysis, PS_LIST_TAIL, "NPSFSTAR", PS_META_REPLACE, "Number of stars used to make PSF", 0);
+    psMetadataAddBool(readout->analysis, PS_LIST_TAIL, "PSFMODEL", PS_META_REPLACE, "Valid PSF Model?", false);
 
     return true;
Index: branches/simtest_nebulous_branches/psphot/src/psphotCleanup.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotCleanup.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotCleanup.c	(revision 27840)
@@ -14,9 +14,6 @@
     psFree (config);
 
-    psThreadPoolFinalize ();
-    psTimerStop ();
     psMemCheckCorruption (stderr, true);
     pmModelClassCleanup ();
-    psTimeFinalize ();
     pmSourceFitSetDone ();
     pmConceptsDone ();
Index: branches/simtest_nebulous_branches/psphot/src/psphotCullPeaks.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotCullPeaks.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotCullPeaks.c	(revision 27840)
@@ -5,9 +5,10 @@
  */
 psErrorCode
-psphotCullPeaks(const psImage *image,   // the image wherein lives the footprint
-                const psImage *weight,  // corresponding variance image
+psphotCullPeaks(const pmReadout *readout,   // the image wherein lives the footprint
                 const psMetadata *recipe,
                 psArray *footprints) {  // array of pmFootprints
+
     bool status = false;
+
     float nsigma_delta = psMetadataLookupF32(&status, recipe, "FOOTPRINT_CULL_NSIGMA_DELTA");
     if (!status) {
@@ -22,14 +23,26 @@
         fPadding = 0;
     }
-    const float skyStdev = psMetadataLookupF32(NULL, recipe, "SKY_STDEV");
+    const float skyStdev = psMetadataLookupF32(&status, readout->analysis, "SKY_STDEV");
+    psAssert (status, "cannot find SKY_STDEV in readout->analysis");
+
     const float min_threshold = nsigma_min*skyStdev;
     
     for (int i = 0; i < footprints->n; i++) {
+	// if (i % 50 == 0) fprintf (stderr, "cull %d\n", i);
 	pmFootprint *fp = footprints->data[i];
-	if (pmFootprintCullPeaks(image, weight, fp, nsigma_delta, fPadding, min_threshold) != PS_ERR_NONE) {
+	if (fp->npix > 30000) {
+	    fprintf (stderr, "big footprint: %f %f to %f %f (%d pix)\n", fp->bbox.x0, fp->bbox.y0, fp->bbox.x1, fp->bbox.y1, fp->npix);
+	}
+	psTimerStart ("psphot.cull.footprints");
+	if (pmFootprintCullPeaks(readout->image, readout->variance, fp, nsigma_delta, fPadding, min_threshold) != PS_ERR_NONE) {
 	    return psError(PS_ERR_UNKNOWN, false, "Culling pmFootprint %d", fp->id);
 	}
+	float dtime = psTimerMark ("psphot.cull.footprints");
+	if (dtime > 1.0) {
+	    fprintf (stderr, "slow cull for %d (%f sec)\n", i, dtime);
+	}
     }
-    
     return PS_ERR_NONE;
 }
+
+// psLogMsg ("psphot", PS_LOG_INFO, "%ld peaks, %ld total footprints: %f sec\n", detections->peaks->n, detections->footprints->n, 
Index: branches/simtest_nebulous_branches/psphot/src/psphotDeblendSatstars.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotDeblendSatstars.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotDeblendSatstars.c	(revision 27840)
@@ -1,8 +1,27 @@
 # include "psphotInternal.h"
 
-bool psphotDeblendSatstars (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotDeblendSatstars (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotDeblendSatstarsReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on saturated star deblend analysis for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotDeblendSatstarsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int fileIndex) {
 
     int N;
     pmSource *source;
+    bool status;
 
     psTimerStart ("psphot.deblend.sat");
@@ -11,5 +30,26 @@
     float SAT_MIN_RADIUS = 5.0;
 
-    bool status;
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, fileIndex); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping satstar blend");
+	return true;
+    }
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
     pmCell *cell = readout->parent;
     float SATURATION = 0.75*psMetadataLookupF32 (&status, cell->concepts, "CELL.SATURATION");
@@ -49,5 +89,8 @@
 	psVector *xVec = contour->data[0];
 	psVector *yVec = contour->data[1];
-	if (xVec->n < 5) continue;
+	if (xVec->n < 5) {
+	    psFree(contour);
+	    continue;
+	}
 
 	// find the center of the contour (let's just use mid[x,y])
Index: branches/simtest_nebulous_branches/psphot/src/psphotDefineFiles.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotDefineFiles.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotDefineFiles.c	(revision 27840)
@@ -126,4 +126,13 @@
     }
 
+    if (psMetadataLookupPtr(NULL, config->arguments, "SRCTEXT")) {
+	// XXX cannot use pmFPAfileDefineFromArgs: this is explicitly a FITS-based I/O function
+	// supply the attach the 
+	if (!psphotLoadSRCTEXT(input->fpa, config)) {
+            psError(PSPHOT_ERR_CONFIG, false, "Failed to load PSPHOT.INPUT.TEXT");
+            return status;
+        }
+    }
+
     if (psMetadataLookupPtr(NULL, config->arguments, "PSPHOT.PSF")) {
         pmFPAfileBindFromArgs(&status, input, config, "PSPHOT.PSF.LOAD", "PSPHOT.PSF");
Index: branches/simtest_nebulous_branches/psphot/src/psphotDiagnosticPlots.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotDiagnosticPlots.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotDiagnosticPlots.c	(revision 27840)
@@ -68,4 +68,5 @@
     if (!psVectorStats (stats, histogram->nums, NULL, NULL, 0)) {
 	psError(PS_ERR_UNKNOWN, false, "failure to measure stats for histogram");
+	psFree(histogram);
         return false;
     }
@@ -108,4 +109,5 @@
     if (!psVectorStats (stats, histogram->nums, NULL, NULL, 0)) {
 	psError(PS_ERR_UNKNOWN, false, "failure to measure stats for histogram");
+	psFree(histogram);
         return false;
     }
Index: branches/simtest_nebulous_branches/psphot/src/psphotEfficiency.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotEfficiency.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotEfficiency.c	(revision 27840)
@@ -0,0 +1,512 @@
+# include "psphotInternal.h"
+
+#define MODEL_MASK (PM_MODEL_STATUS_NONCONVERGE | PM_MODEL_STATUS_OFFIMAGE | \
+                    PM_MODEL_STATUS_BADARGS | PM_MODEL_STATUS_LIMITS) // Mask to apply to models
+
+# define TESTING 0
+
+// Calculate the limiting magnitude for an image
+//
+// We limit ourselves to calculating the peak flux in the smoothed image, which should be close (modulo
+// non-Gaussian PSF) to the limiting flux in the un-smoothed original image.
+static bool effLimit(float *magLim,           // Limiting magntiude, to return
+                     int *radius,             // Radius for fake sources, to return
+                     float *minFlux,          // Minimum flux for fake sources, to return
+                     float *norm,             // Normalisation of PSF (conversion: peak --> integrated flux)
+                     float *covarFactor,// Covariance factor
+                     const pmReadout *ro,     // Readout of interest
+                     pmPSF *psf,              // Point-spread function
+                     float thresh,            // Threshold for source identification
+                     float smoothSigma,       // Gaussian smoothing sigma
+                     float smoothNsigma,      // Smoothing limit
+                     psImageMaskType maskVal  // Value to mask
+                     )
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_VARIANCE(ro, false);
+    assert(magLim);
+    assert(radius);
+    assert(minFlux);
+    assert(norm);
+    assert(covarFactor);
+
+    psKernel *kernel = psImageSmoothKernel(smoothSigma, smoothNsigma); // Kernel used for smoothing
+    *covarFactor = psImageCovarianceCalculateFactor(kernel, ro->covariance);
+    psFree(kernel);
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for variance
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);        // Random number generator
+    if (!psImageBackground(stats, NULL, ro->variance, ro->mask, maskVal, rng) ||
+        !isfinite(stats->robustMedian)) {
+        psError(PSPHOT_ERR_DATA, false, "Unable to determine mean variance");
+        psFree(stats);
+        psFree(rng);
+        return false;
+    }
+    psFree(rng);
+    float meanVar = stats->robustMedian; // Mean variance
+    psFree(stats);
+
+    // Need to normalise out difference between Gaussian and real PSF
+    pmModel *normModel = pmModelFromPSFforXY(psf, 0.5 * ro->variance->numCols,
+                                             0.5 * ro->variance->numRows, 1.0); // Model for normalisation
+    if (!normModel || (normModel->flags & MODEL_MASK)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate PSF model.");
+        psFree(normModel);
+        return false;
+    }
+    *norm = normModel->modelFlux(normModel->params); // Total flux for peak of 1.0
+    psFree(normModel);
+
+    // The signal-to-noise of the smoothed image is: S/N ~ I_smooth / sqrt(variance * factor)
+    // since the variance factor tells us the variance in the smoothed image.  Now, the trick is working
+    // out what the intensity in the smoothed image is, and how it is related to the flux.  We are
+    // convolving Io G_* with G_PSF/2pi.w^2, where G_* is the approximately-Gaussian PSF of the star,
+    // G_PSF is the pretty-close-matching Gaussian of the convolution kernel, Io is the peak flux in the
+    // unsmoothed image, and w is the width of the Gaussian.  Now, a normalised 2D Gaussian convolved
+    // with itself has a normalisation of 1/2pi(w^2+w^2) = 1/4pi.w^2.  Therefore:
+    // I_smooth = Flux / 2*norm.
+    float peakLim = thresh * sqrtf(meanVar * *covarFactor); // Limiting peak value in smoothed image
+    float fluxLim = 2.0 * *norm * peakLim; // Limiting flux in original
+    *magLim = -2.5 * log10f(fluxLim);
+    psTrace("psphot.fake", 1, "Limiting peak: %f\n", peakLim);
+    psTrace("psphot.fake", 1, "Limiting flux: %f\n", fluxLim);
+    psTrace("psphot.fake", 1, "Limiting mag: %f\n", *magLim);
+
+    *radius = smoothSigma * smoothNsigma;
+
+    *minFlux = 0.1 * sqrtf(meanVar);
+
+    return true;
+}
+
+/// Generate a fake image and add it in to the existing readout
+static bool effGenerate(psImage **xSrc, psImage **ySrc, // Positions of sources
+                        const pmReadout *ro,            // Readout of interest
+                        const pmPSF *psf,               // Point-spread function
+                        const psVector *magOffsets,     // Magnitude offsets for fake sources
+                        int numSources,                 // Number of fake sources for each bin
+                        float refMag,                   // Reference magnitude
+                        int radius,                     // Radius for fake sources
+                        float minFlux                   // Minimum flux for fake sources
+                        )
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+    PS_ASSERT_VECTOR_NON_NULL(magOffsets, false);
+    PS_ASSERT_VECTOR_TYPE(magOffsets, PS_TYPE_F32, false);
+    assert(xSrc);
+    assert(ySrc);
+
+    int numBins = magOffsets->n;                                    // Number of bins
+    int numCols = ro->image->numCols, numRows = ro->image->numRows; // Size of image
+
+    *xSrc = psImageRecycle(*xSrc, numSources, numBins, PS_TYPE_F32);
+    *ySrc = psImageRecycle(*ySrc, numSources, numBins, PS_TYPE_F32);
+
+    psVector *xAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+    psVector *yAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+    psVector *magAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+    for (int i = 0, index = 0; i < numBins; i++) {
+        float mag = refMag + magOffsets->data.F32[i]; // Instrumental magnitude of sources
+
+        for (int j = 0; j < numSources; j++, index++) {
+            xAll->data.F32[index] = psRandomUniform(rng) * numCols;
+            yAll->data.F32[index] = psRandomUniform(rng) * numRows;
+            magAll->data.F32[index] = mag;
+        }
+        memcpy((*xSrc)->data.F32[i], &xAll->data.F32[i * numSources],
+               numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+        memcpy((*ySrc)->data.F32[i], &yAll->data.F32[i * numSources],
+               numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+    }
+    psFree(rng);
+
+    bool oldThreads = pmReadoutFakeThreads(true); // Old threading status
+
+    pmReadout *fakeRO = pmReadoutAlloc(NULL); // Fake readout
+    if (!pmReadoutFakeFromVectors(fakeRO, numCols, numRows, xAll, yAll, magAll,
+                                  NULL, NULL, psf, minFlux, radius, false, true)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate fake image");
+        psFree(fakeRO);
+        psFree(magAll);
+        psFree(xAll);
+        psFree(yAll);
+        return false;
+    }
+    psFree(magAll);
+    psFree(xAll);
+    psFree(yAll);
+
+    pmReadoutFakeThreads(oldThreads);
+
+    psBinaryOp(ro->image, ro->image, "+", fakeRO->image);
+    psFree(fakeRO);
+
+    return true;
+}
+
+bool psphotEfficiency (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+	if (!psphotEfficiencyReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to measure detection efficiency for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+// Determine detection efficiency
+bool psphotEfficiencyReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe)
+{
+    bool status = true;
+
+    psTimerStart("psphot.fake");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *realSources = detections->allSources;
+    psAssert (realSources, "missing sources?");
+
+    // XXX do we need to skip this step if we do not have a psf?
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    psAssert (psf, "missing psf?");
+
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PS_ASSERT_PTR_NON_NULL(psf, false);
+    PS_ASSERT_METADATA_NON_NULL(recipe, false);
+
+    // Collect recipe information
+    float smoothNsigma = psMetadataLookupF32(&status, recipe, "PEAKS_SMOOTH_NSIGMA"); // Smoothing limit
+    psAssert (status && isfinite(smoothNsigma), "Unable to find PEAKS_SMOOTH_NSIGMA in recipe (or invalid value)");
+
+    float thresh = psMetadataLookupF32(&status, recipe, "PEAKS_NSIGMA_LIMIT_2");
+    psAssert (status && isfinite(thresh), "Unable to find PEAKS_NSIGMA_LIMIT_2 in recipe (or invalid value)");
+
+    psVector *magOffsets = psMetadataLookupVector(&status, recipe, "EFF.MAG"); // Magnitude offsets
+    psAssert (status, "Unable to find EFF.MAG F32 vector in recipe");
+    psAssert (magOffsets->type.type == PS_TYPE_F32, "Unable to find EFF.MAG F32 vector in recipe");
+
+    int numSources = psMetadataLookupS32(&status, recipe, "EFF.NUM"); // Number of sources for each bin
+    psAssert (status && (numSources > 0), "Unable to find EFF.NUM in recipe (or invalid value)");
+
+    float minGauss = psMetadataLookupF32(&status, recipe, "PEAKS_MIN_GAUSS"); // Minimum valid fraction of kernel
+    psAssert (status && isfinite(minGauss), "PEAKS_MIN_GAUSS is not set in recipe (or invalid)");
+
+    // find the PSF size information (why is this not part of the psf structure?)
+    float fwhmMajor = psMetadataLookupF32(NULL, readout->analysis, "FWHM_MAJ"); // PSF size in x
+    float fwhmMinor = psMetadataLookupF32(NULL, readout->analysis, "FWHM_MIN"); // PSF size in y
+    if (!isfinite(fwhmMajor) || !isfinite(fwhmMinor) || fwhmMajor == 0.0 || fwhmMinor == 0.0) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find FWHM_MAJ and FWHM_MIN in readout->analysis");
+        return false;
+    }
+
+    float smoothSigma = 0.5*(fwhmMajor + fwhmMinor) / (2.0*sqrtf(2.0*log(2.0))); // Gaussian smoothing sigma
+    int numCols = readout->image->numCols, numRows = readout->image->numRows; // Size of image
+    int numBins = magOffsets->n;                          // Number of bins
+
+    psImageMaskType maskVal = psMetadataLookupImageMask(NULL, recipe, "MASK.PSPHOT"); // Value to mask
+
+    // remove all sources, adding noise for subtracted sources
+    psphotRemoveAllSources(realSources, recipe);
+
+#if TESTING
+    {
+        psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
+        for (int y = 0; y < numRows; y++) {
+            for (int x = 0; x < numCols; x++) {
+                readout->image->data.F32[y][x] = psRandomGaussian(rng);
+                readout->variance->data.F32[y][x] = 1.0;
+                readout->mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = 0;
+            }
+        }
+        psFree(readout->covariance);
+        readout->covariance = NULL;
+        psFree(rng);
+    }
+#endif
+
+
+
+    float magLim;                       // Guess at limiting magnitude
+    int radius;                         // Radius for fake sources
+    float minFlux;                      // Minimum flux for fake sources
+    float norm;                         // Normalisation of PSF
+    float covarFactor;                  // Covariance factor
+    if (!effLimit(&magLim, &radius, &minFlux, &norm, &covarFactor, readout,
+                  psf, thresh, smoothSigma, smoothNsigma, maskVal)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine limits for image");
+        return false;
+    }
+
+#if TESTING
+    psphotSaveImage(NULL, readout->image, "orig_image.fits");
+    psphotSaveImage(NULL, readout->variance, "orig_variance.fits");
+    psphotSaveImage(NULL, readout->mask, "orig_mask.fits");
+#endif
+
+    psImage *xFake = NULL, *yFake = NULL; // Coordinates of sources, each bin in a row
+    if (!effGenerate(&xFake, &yFake, readout, psf, magOffsets,
+                     numSources, magLim, radius, minFlux)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate fake sources");
+        psFree(xFake);
+        psFree(yFake);
+        return false;
+    }
+
+#if TESTING
+    psphotSaveImage(NULL, readout->image, "fake_image.fits");
+    psphotSaveImage(NULL, readout->variance, "fake_variance.fits");
+    psphotSaveImage(NULL, readout->mask, "fake_mask.fits");
+#endif
+
+    // XXX Could speed this up significantly by only convolving the central pixels of each fake source
+    psVector *significance = NULL;       // Significance image
+    {
+        int num = numSources * numBins; // Total number of sources
+        // Pixel coordinates of sources
+        psVector *xPix = psVectorAlloc(num, PS_TYPE_S32);
+        psVector *yPix = psVectorAlloc(num, PS_TYPE_S32);
+        for (int i = 0, index = 0; i < numBins; i++) {
+            for (int j = 0; j < numSources; j++, index++) {
+                float x = xFake->data.F32[i][j];
+                float y = yFake->data.F32[i][j];
+                xPix->data.S32[index] = PS_MIN(x + 0.5, numCols - 1);
+                yPix->data.S32[index] = PS_MIN(y + 0.5, numRows - 1);
+            }
+        }
+
+        psVector *convImage = psImageSmoothMaskPixels(readout->image, readout->mask, maskVal, xPix, yPix,
+                                                      smoothSigma, smoothNsigma, minGauss); // Convolved image
+        if (!convImage) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to smooth image pixels");
+            psFree(xPix);
+            psFree(yPix);
+            psFree(xFake);
+            psFree(yFake);
+            return false;
+        }
+        psVector *convVar = psImageSmoothMaskPixels(readout->variance, readout->mask, maskVal, xPix, yPix,
+                                                    smoothSigma, smoothNsigma, minGauss); // Convolved varianc
+        if (!convVar) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to smooth variance pixels");
+            psFree(xPix);
+            psFree(yPix);
+            psFree(xFake);
+            psFree(yFake);
+            return false;
+        }
+
+        float factor = 1.0 / covarFactor; // Correction for covariance
+
+        const psImage *mask = readout->mask;  // Mask for readout
+        for (int i = 0; i < num; i++) {
+            float imageVal = convImage->data.F32[i]; // Image value
+            float varVal = convVar->data.F32[i]; // Variance value
+            int x = xPix->data.S32[i], y = yPix->data.S32[i]; // Coordinates
+            if (imageVal < 0 || varVal <= 0 || (mask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & maskVal)) {
+                convImage->data.F32[i] = 0.0;
+            } else {
+                convImage->data.F32[i] = factor * PS_SQR(imageVal) / varVal;
+            }
+        }
+
+        psFree(xPix);
+        psFree(yPix);
+
+        significance = convImage;
+        psFree(convVar);
+    }
+
+    thresh *= thresh;                   // "Significance" is actually significance-squared
+
+    psVector *count = psVectorAlloc(numBins, PS_TYPE_S32); // Number of sources found in each bin
+    psArray *fakeSources = psArrayAlloc(numSources);            // Fake sources in each bin
+    psArray *fakeSourcesAll = psArrayAllocEmpty(numSources * numBins); // All fake sources
+    for (int i = 0, index = 0; i < numBins; i++) {
+        int numFound = 0;               // Number found
+
+        // Determine extraction size
+        float mag = magLim + magOffsets->data.F32[i]; // Magnitude for bin
+        float peak = powf(10.0, -0.4 * mag) / norm;   // Peak flux
+        pmModel *model = pmModelFromPSFforXY(psf, numCols / 2.0, numRows / 2.0, peak); // Model for source
+        if (!model || (model->flags & MODEL_MASK)) {
+            psError(PS_ERR_UNKNOWN, true, "Unable to generate model for bin %d", i);
+            psFree(model);
+            return false;
+        }
+        float sourceRadius = PS_MAX(radius, model->modelRadius(model->params, minFlux)); // Radius for source
+        psFree(model);
+
+        psArray *sources = psArrayAllocEmpty(numSources); // Sources in this bin
+        for (int j = 0; j < numSources; j++, index++) {
+            // Coordinates of interest
+            float sig = significance->data.F32[index]; // Significance of pixel
+            if (sig > thresh) {
+                pmSource *source = pmSourceAlloc(); // Fake source
+                float x = xFake->data.F32[i][j];
+                float y = yFake->data.F32[i][j];
+                source->peak = pmPeakAlloc(x, y, sig, PM_PEAK_LONE);
+                if (!pmSourceDefinePixels(source, readout, x, y, sourceRadius)) {
+                    psErrorClear();
+                    continue;
+                }
+                source->peak->xf = x;
+                source->peak->yf = y;
+                source->modelPSF = pmModelFromPSFforXY(psf, x, y, 2.0 * sqrtf(sig));
+                source->type = PM_SOURCE_TYPE_STAR;
+
+                numFound++;
+                psArrayAdd(sources, sources->n, source);
+                psArrayAdd(fakeSourcesAll, fakeSourcesAll->n, source);
+                psFree(source);
+            }
+        }
+        fakeSources->data[i] = sources;
+        count->data.S32[i] = numFound;
+    }
+    psFree(xFake);
+    psFree(yFake);
+    psFree(significance);
+
+    if (!psphotFitSourcesLinearReadout(recipe, readout, fakeSourcesAll, psf, true)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to perform linear fit on fake sources.");
+        psFree(fakeSources);
+        psFree(count);
+        return false;
+    }
+
+    // Disable aperture corrections (save current value)
+    pmTrend2D *apTrend = psf->ApTrend;  // Aperture trend
+    psf->ApTrend = NULL;
+
+    if (!psphotMagnitudesReadout(config, recipe, view, readout, fakeSourcesAll, psf)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to measure magnitudes of fake sources.");
+        psFree(fakeSources);
+        psFree(count);
+        psf->ApTrend = apTrend; // Casting away const!
+        return false;
+    }
+    psFree(fakeSourcesAll);
+
+    // Replace aperture corrections
+    psf->ApTrend = apTrend;
+
+    psVector *magDiffMean = psVectorAlloc(numBins, PS_TYPE_F32); // Mean difference in magnitude for each bin
+    psVector *magDiffStdev = psVectorAlloc(numBins, PS_TYPE_F32); // Stdev of diff in magnitude for each bin
+    psVector *magErrMean = psVectorAlloc(numBins, PS_TYPE_F32); // Mean error in magnitude for each bin
+    psVector *magDiff = psVectorAlloc(numSources, PS_TYPE_F32);   // Magnitude differences
+    psVector *magErr = psVectorAlloc(numSources, PS_TYPE_F32);   // Magnitude errors
+    psVector *magMask = psVectorAlloc(numSources, PS_TYPE_VECTOR_MASK); // Mask for magnitude errors
+    psStats *magStats = psStatsAlloc(PS_STAT_SAMPLE_MEAN | PS_STAT_SAMPLE_STDEV |
+                                     PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV); // Statistics
+    for (int i = 0; i < numBins; i++) {
+        psStatsInit(magStats);
+        psVectorInit(magMask, 0);
+
+#if TESTING
+        psString name = NULL;
+        psStringAppend(&name, "fake_%d.dat", i);
+        FILE *file = fopen(name, "w");
+        psFree(name);
+#endif
+
+        float magRef = magLim + magOffsets->data.F32[i]; // Reference magnitude
+        psArray *sources = fakeSources->data[i];         // Sources in bin
+        for (int j = 0; j < sources->n; j++) {
+            pmSource *source = sources->data[j]; // Source of interest
+            if (!source || !isfinite(source->psfMag)) {
+                magMask->data.PS_TYPE_VECTOR_MASK_DATA[j] = 0xFF;
+                continue;
+            }
+
+#if TESTING
+            fprintf(file, "%f %f %f %f %f %f %f\n", source->peak->xf, source->peak->yf,
+                    source->modelPSF->params->data.F32[PM_PAR_XPOS],
+                    source->modelPSF->params->data.F32[PM_PAR_YPOS],
+                    magRef, source->psfMag, source->errMag);
+#endif
+            magDiff->data.F32[j] = source->psfMag - magRef;
+            magErr->data.F32[j] = source->errMag;
+        }
+        magDiff->n = sources->n;
+        magMask->n = sources->n;
+        magErr->n = sources->n;
+        if (!psVectorStats(magStats, magDiff, NULL, magMask, 0xFF)) {
+            // Probably because we don't have enough sources
+            psErrorClear();
+        }
+
+        if (isfinite(magStats->robustMedian) && isfinite(magStats->robustStdev)) {
+            magDiffMean->data.F32[i] = magStats->robustMedian;
+            magDiffStdev->data.F32[i] = magStats->robustStdev;
+        } else {
+            magDiffMean->data.F32[i] = magStats->sampleMean;
+            magDiffStdev->data.F32[i] = magStats->sampleStdev;
+        }
+
+        if (!psVectorStats(magStats, magErr, NULL, magMask, 0xFF)) {
+            // Probably because we don't have enough sources
+            psErrorClear();
+        }
+
+        if (isfinite(magStats->robustMedian)) {
+            magErrMean->data.F32[i] = magStats->robustMedian;
+        } else {
+            magErrMean->data.F32[i] = magStats->sampleMean;
+        }
+
+#if TESTING
+        fclose(file);
+#endif
+    }
+
+    psFree(magStats);
+    psFree(magDiff);
+    psFree(magMask);
+    psFree(magErr);
+
+    psFree(fakeSources);
+
+    pmDetEff *de = pmDetEffAlloc(magLim, numSources, numBins); // Detection efficiency
+    de->magOffsets = psVectorCopy(NULL, magOffsets, PS_TYPE_F32);
+    de->counts = count;
+    de->magDiffMean = magDiffMean;
+    de->magDiffStdev = magDiffStdev;
+    de->magErrMean = magErrMean;
+
+    psMetadataAddPtr(readout->analysis, PS_LIST_TAIL, PM_DETEFF_ANALYSIS, PS_META_REPLACE | PS_DATA_UNKNOWN,
+                     "Detection efficiency", de);
+    psFree(de);
+
+    psLogMsg("psphot", PS_LOG_INFO, "Detection efficiency: %lf sec\n", psTimerClear("psphot.fake"));
+
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotEllipticalContour.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotEllipticalContour.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotEllipticalContour.c	(revision 27840)
@@ -0,0 +1,179 @@
+# include "psphotInternal.h"
+
+// model parameters
+enum {PAR_PHI, PAR_EPSILON, PAR_RMIN};
+psF32 psphotEllipticalContourFunc (psVector *deriv, const psVector *params, const psVector *coord);
+
+bool psphotEllipticalContour (pmSource *source) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->extpars->radFlux, "missing radFlux");
+
+    pmSourceRadialFlux *profile = source->extpars->radFlux;
+    pmSourceExtendedPars *extpars = source->extpars;
+
+    // use LMM to fit theta vs radius to an ellipse
+    psVector *theta = profile->theta;
+    psVector *radius = profile->isophotalRadii;
+
+    // find Rmin and Rmax for the initial guess
+    float Rmin = radius->data.F32[0];
+    float Rmax = radius->data.F32[0];
+
+    // arrays to hold the data to be fitted
+    // we fit x and y vs theta in separate passes.
+    psArray *x = psArrayAllocEmpty(2*radius->n);
+    psVector *y = psVectorAllocEmpty(2*radius->n, PS_TYPE_F32);
+    psVector *yErr = psVectorAllocEmpty(2*radius->n, PS_TYPE_F32);
+
+    int n = 0;
+    for (int i = 0; i < radius->n; i++) {
+	if (!isfinite(radius->data.F32[i])) continue;
+
+	psVector *coord = NULL;
+
+	// Rx coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 0.0;
+	coord->data.F32[0] = theta->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = radius->data.F32[i]*cos(theta->data.F32[i]);
+	yErr->data.F32[n] = 1000.0;
+	n++;
+
+	// Ry coordinate value
+	coord = psVectorAlloc (2, PS_TYPE_F32);
+	coord->data.F32[1] = 1.0;
+	coord->data.F32[0] = theta->data.F32[i];
+	x->data[n] = coord;
+	y->data.F32[n] = radius->data.F32[i]*sin(theta->data.F32[i]);
+	yErr->data.F32[n] = 1000.0;
+	n++;
+
+	// check the radius range
+	Rmin = MIN (Rmin, radius->data.F32[i]);
+	Rmax = MAX (Rmax, radius->data.F32[i]);
+    }	
+    x->n = n;
+    y->n = n;
+    yErr->n = n;
+
+    if (n < 4) {
+	psFree (x);
+	psFree (y);
+	psFree (yErr);
+	return false;
+    }
+
+    psVector *params = psVectorAlloc (3, PS_TYPE_F32);
+    
+    // psTraceSetLevel ("psLib.math.psMinimizeLMChi2", 7);
+    
+    // create the minimization constraints
+    psMinConstraint *constraint = psMinConstraintAlloc();
+
+    // XXX for now, no parameter masks, skip checkLimits
+    // XXX might help to add a limit to the angle or re-parameterize the ellipse in terms of Rxx, Ryy, Rxy
+    // constraint->checkLimits = psastroModelBoresiteLimits;
+
+    params->data.F32[PAR_PHI]     = 0.0;
+    params->data.F32[PAR_EPSILON] = Rmin / Rmax;
+    params->data.F32[PAR_RMIN]    = Rmin;
+
+    psMinimization *myMin = psMinimizationAlloc (25, 0.001);
+    psImage *covar = psImageAlloc (params->n, params->n, PS_TYPE_F32);
+    
+    // XXX skip the weights for now
+    psMinimizeLMChi2(myMin, covar, params, constraint, x, y, yErr, psphotEllipticalContourFunc);
+
+    /// XXX rationalize? if epsilon > 1, flip major and minor axes (rotate by 90 degrees)
+    if (params->data.F32[PAR_EPSILON] < 1.0) {
+	extpars->axes.major = params->data.F32[PAR_RMIN] / params->data.F32[PAR_EPSILON];
+	extpars->axes.minor = params->data.F32[PAR_RMIN];
+	extpars->axes.theta = params->data.F32[PAR_PHI];
+    } else {
+	extpars->axes.major = params->data.F32[PAR_RMIN];
+	extpars->axes.minor = params->data.F32[PAR_RMIN] / params->data.F32[PAR_EPSILON];
+	extpars->axes.theta = params->data.F32[PAR_PHI] + 0.5*M_PI;
+    }
+
+    psTrace ("psphot", 4, "# fitted values:\n");
+    psTrace ("psphot", 4, "Phi:   %f\n", extpars->axes.theta*PS_DEG_RAD);
+    psTrace ("psphot", 4, "Rmaj:  %f\n", extpars->axes.major);
+    psTrace ("psphot", 4, "Rmin:  %f\n", extpars->axes.minor);
+    
+    // show the results
+    // psphotPetrosianVisualEllipticalContour (profile, extpars);
+
+    psFree (x);
+    psFree (y);
+    psFree (yErr);
+    psFree (params);
+    psFree (covar);
+    psFree (myMin);
+    psFree (constraint);
+
+    return true;
+}
+
+/**
+ * the full chisq is built of two associated sums over coordinates:
+ * chisq = sum ((Rx_obs - Rx_fit(t))^2 + (Ry_obs - Ry_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 psphotEllipticalContourFunc (psVector *deriv, const psVector *params, const psVector *coord) {
+
+    static int pass = 0;
+
+    psF32 *par = params->data.F32;
+
+    float alpha = coord->data.F32[0];
+
+    float cs_alpha = cos(alpha);
+    float sn_alpha = sin(alpha);
+
+    float cs_phi = cos(alpha - par[PAR_PHI]);
+    float sn_phi = sin(alpha - par[PAR_PHI]);
+
+    float r     = 1.0 / sqrt(SQ(sn_phi) + SQ(par[PAR_EPSILON]*cs_phi));
+    float r3    = pow(r, 3.0);
+    float drdE  = -0.5 * r3 * SQ(cs_phi) * 2.0 * par[PAR_EPSILON];
+    float drdP  = -0.5 * r3 * (SQ(par[PAR_EPSILON]) - 1) * 2.0 * cs_phi * sn_phi;
+
+    // value is X
+    // if (coord->data.F32[1] == 0) {
+    if (pass == 0) {
+	pass = 1;
+
+	float value = par[PAR_RMIN]*cs_alpha*r;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_RMIN]    = r*cs_alpha;
+	    dPAR[PAR_EPSILON] = par[PAR_RMIN]*cs_alpha*drdE;
+	    dPAR[PAR_PHI]     = 4.0*par[PAR_RMIN]*cs_alpha*drdP;
+	}
+	return (value);
+    }  
+
+    // value is Y
+    // if (coord->data.F32[1] == 1) {
+    if (pass == 1) {
+	pass = 0;
+
+	float value = par[PAR_RMIN]*sn_alpha*r;
+
+	if (deriv) {
+	    psF32 *dPAR = deriv->data.F32;
+	    dPAR[PAR_RMIN]    = r*sn_alpha;
+	    dPAR[PAR_EPSILON] = par[PAR_RMIN]*sn_alpha*drdE;
+	    dPAR[PAR_PHI]     = 4.0*par[PAR_RMIN]*sn_alpha*drdP;
+	}
+	return (value);
+    }  
+
+    psAbort ("programming error: invalid coordinate");
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotEllipticalProfile.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotEllipticalProfile.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotEllipticalProfile.c	(revision 27840)
@@ -0,0 +1,90 @@
+# include "psphotInternal.h"
+
+bool psphotEllipticalProfile (pmSource *source, bool RAW_RADIUS) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->pixels, "missing pixels");
+
+    pmSourceExtendedPars *extpars = source->extpars;
+
+    if (!source->extpars->ellipticalFlux) {
+	source->extpars->ellipticalFlux = pmSourceEllipticalFluxAlloc();
+    }
+    pmSourceEllipticalFlux *profile = source->extpars->ellipticalFlux;
+
+    profile->radiusElliptical = psVectorAllocEmpty(100, PS_TYPE_F32);
+    profile->fluxElliptical = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    psVector *radius = profile->radiusElliptical;
+    psVector *flux = profile->fluxElliptical;
+
+    // the psEllipse functions use z = 0.5(x/Sxx)^2 + 0.5(y/Syy)^2 + x y Sxy
+    // which are converted to z = 0.5(x/a)^2 + 0.5(y/b)^2
+    // we have major and minor axes of a specific ellipse with r^2 = (x/A)^2 + (y/B)^2
+    // a = A / sqrt(2)
+
+    // we have the shape parameters of the elliptical contour at the reference isophote.
+    // use the axis ratio (major/minor) to rescale the radial profile so that 1 pixel
+    // along the major axis is 1 pixel, and a smaller amount on the minor axis
+
+    psEllipseAxes axes;
+    if (RAW_RADIUS) {
+	// force circular profile
+	axes.major = M_SQRT1_2;
+	axes.minor = M_SQRT1_2;
+    } else {
+	axes.major = M_SQRT1_2;
+	axes.minor = M_SQRT1_2 * (extpars->axes.minor / extpars->axes.major);
+    }
+
+    // axes.major = 1.0;
+    // axes.minor = extpars->axes.minor / extpars->axes.major;
+
+    axes.theta = extpars->axes.theta;
+    psEllipseShape shape = psEllipseAxesToShape (axes);
+
+    float Sxx = shape.sx;
+    float Sxy = shape.sxy;
+    float Syy = shape.sy;
+
+    // XXX drop these two vectors?
+    psVector *radiusRaw = psVectorAllocEmpty(100, PS_TYPE_F32);
+    psVector *fluxRaw = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    for (int iy = 0; iy < source->pixels->numRows; iy++) {
+	for (int ix = 0; ix < source->pixels->numCols; ix++) {
+
+	    // 0.5 PIX: get radius as a function of pixel coord
+	    float x = ix + 0.5 - source->peak->xf + source->pixels->col0;
+	    float y = iy + 0.5 - source->peak->yf + source->pixels->row0;
+
+	    float r2 = 0.5*PS_SQR(x/Sxx) + 0.5*PS_SQR(y/Syy) + x*y*Sxy;
+	    float Rraw = hypot(x, y);
+
+	    psVectorAppend(radius, sqrt(r2));
+	    psVectorAppend(flux, source->pixels->data.F32[iy][ix]);
+
+	    psVectorAppend(radiusRaw, Rraw);
+	    psVectorAppend(fluxRaw, source->pixels->data.F32[iy][ix]);
+	}
+    }
+
+    // psVector *radiusRaw = psVectorAllocEmpty(100, PS_TYPE_F32);
+    // psVector *fluxRaw = psVectorAllocEmpty(100, PS_TYPE_F32);
+    // for (int i = 0; i < profile->radii->n; i++) {
+    //   psVector *r = profile->radii->data[i];
+    //   psVector *f = profile->fluxes->data[i];
+    //   for (int j = 0; j < r->n; j++) {
+    // 	psVectorAppend(radiusRaw, r->data.F32[j]);
+    // 	psVectorAppend(fluxRaw, f->data.F32[j]);
+    //   }
+    // }
+
+    psphotPetrosianVisualProfileRadii (radius, flux, radiusRaw, fluxRaw, source->peak->flux, 0.0);
+    // psphotPetrosianVisualProfileByAngle (radius, flux);
+
+    psFree (radiusRaw);
+    psFree (fluxRaw);
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotErrorCodes.dat
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotErrorCodes.dat	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotErrorCodes.dat	(revision 27840)
@@ -2,4 +2,5 @@
 # This file is used to generate psphotErrorClasses.h
 #
+BASE = 3000		First value we use
 UNKNOWN			Unknown PM error code
 NOT_IMPLEMENTED		Desired feature is not yet implemented
Index: branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceAnalysis.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceAnalysis.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceAnalysis.c	(revision 27840)
@@ -1,6 +1,34 @@
 # include "psphotInternal.h"
 
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotExtendedSourceAnalysis (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    // perform full non-linear fits / extended source analysis?
+    if (!psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANALYSIS")) {
+	psLogMsg ("psphot", PS_LOG_INFO, "skipping extended source measurements\n");
+	return true;
+    }
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotExtendedSourceAnalysisReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on measure extended source aperture-like parameters for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // aperture-like measurements for extended sources
-bool psphotExtendedSourceAnalysis (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+bool psphotExtendedSourceAnalysisReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
@@ -11,18 +39,32 @@
     int Nkron = 0;
 
+    psTimerStart ("psphot.extended");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping source size");
+	return true;
+    }
+
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
     assert (maskVal);
 
-    // perform full non-linear fits / extended source analysis?
-    if (!psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_ANALYSIS")) {
-	psLogMsg ("psphot", PS_LOG_INFO, "skipping extended source measurements\n");
-	return true;
-    }
+    // XXX require petrosian analysis for non-linear fits? 
 
-    // option to limit analysis to a specific region
-    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
-    psRegion AnalysisRegion = psRegionForImage (readout->image, psRegionFromString (region));
-    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
+    // XXX temporary user-supplied systematic sky noise measurement (derive from background model)
+    float skynoise = psMetadataLookupF32 (&status, recipe, "SKY.NOISE");
 
     // S/N limit to perform full non-linear fits
@@ -35,8 +77,18 @@
     bool doKron         = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_KRON");
 
+# if (0)
+    // if backModel or backStdev are missing, the values of sky and/or skyErr will be set to NAN
+    // XXX use this to set skynoise
+    pmReadout *backModel = psphotSelectBackground (config, view);
+    pmReadout *backStdev = psphotSelectBackgroundStdev (config, view);
+# endif
+
     // source analysis is done in S/N order (brightest first)
     sources = psArraySort (sources, pmSourceSortBySN);
 
-    // XXX some init functions for the extended source recipe options?
+    // option to limit analysis to a specific region
+    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
+    psRegion AnalysisRegion = psRegionForImage (readout->image, psRegionFromString (region));
+    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
 
     // choose the sources of interest
@@ -49,4 +101,7 @@
 	if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
 	if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
+	if (source->mode & PM_SOURCE_MODE_DEFECT) continue;
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) continue;
+	if (!(source->mode & PM_SOURCE_MODE_EXT_LIMIT)) continue;
 
 	// limit selection to some SN limit
@@ -66,12 +121,18 @@
 	Next ++;
 
+	// force source image to be a bit larger...
+	float radius = source->peak->xf - source->pixels->col0;
+	radius = PS_MAX (radius, source->peak->yf - source->pixels->row0);
+	radius = PS_MAX (radius, source->pixels->numRows - source->peak->yf + source->pixels->row0);
+	radius = PS_MAX (radius, source->pixels->numCols - source->peak->xf + source->pixels->col0);
+	pmSourceRedefinePixels (source, readout, source->peak->xf, source->peak->yf, 1.5*radius);
+
 	// if we request any of these measurements, we require the radial profile
 	if (doPetrosian || doIsophotal || doAnnuli || doKron) {
-	    if (!psphotRadialProfile (source, recipe, maskVal)) {
+	    if (!psphotRadialProfile (source, recipe, skynoise, maskVal)) {
 		// all measurements below require the radial profile; skip them all
 		// re-subtract the object, leave local sky
 		psTrace ("psphot", 5, "failed to extract radial profile for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
 		pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-		source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
 		continue;
 	    }
@@ -79,4 +140,16 @@
 	}
 
+	// Petrosian Mags
+	if (doPetrosian) {
+	    if (!psphotPetrosian (source, recipe, skynoise, maskVal)) {
+		psTrace ("psphot", 5, "FAILED petrosian flux & radius for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
+	    } else {
+		psTrace ("psphot", 5, "measured petrosian flux & radius for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
+		Npetro ++;
+		source->mode |= PM_SOURCE_MODE_EXTENDED_STATS;
+	    }
+	}
+
+# if (0)
 	// Isophotal Mags
 	if (doIsophotal) {
@@ -89,16 +162,4 @@
 	    }
 	}
-
-	// Petrosian Mags
-	if (doPetrosian) {
-	    if (!psphotPetrosian (source, recipe, maskVal)) {
-		psTrace ("psphot", 5, "measured petrosian flux & radius for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
-	    } else {
-		psTrace ("psphot", 5, "measured petrosian flux & radius for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
-		Npetro ++;
-		source->mode |= PM_SOURCE_MODE_EXTENDED_STATS;
-	    }
-	}
-
 	// Kron Mags
 	if (doKron) {
@@ -111,26 +172,27 @@
 	    }
 	}
-
-	// Radial Annuli
-	if (doAnnuli) {
-	    if (!psphotAnnuli (source, recipe, maskVal)) {
-		psError(PSPHOT_ERR_UNKNOWN, false, "failure in Annuli analysis");
-		return false;
-	    } 
-	    psTrace ("psphot", 5, "measured annuli for source at %7.1f, %7.1f", source->moments->Mx, source->moments->My);
-	    Nannuli ++;
-	    source->mode |= PM_SOURCE_MODE_EXTENDED_STATS;
-	}
+# endif
 
 	// re-subtract the object, leave local sky
 	pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-	source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
+
+	if (source->extpars) {
+	    psFree(source->extpars->radFlux);
+	    psFree(source->extpars->ellipticalFlux);
+	}
     }
 
-    psLogMsg ("psphot", PS_LOG_INFO, "extended source analysis: %f sec for %d objects\n", psTimerMark ("psphot"), Next);
+    psLogMsg ("psphot", PS_LOG_INFO, "extended source analysis: %f sec for %d objects\n", psTimerMark ("psphot.extended"), Next);
     psLogMsg ("psphot", PS_LOG_INFO, "  %d petrosian\n", Npetro);
     psLogMsg ("psphot", PS_LOG_INFO, "  %d isophotal\n", Nisophot);
     psLogMsg ("psphot", PS_LOG_INFO, "  %d annuli\n", Nannuli);
     psLogMsg ("psphot", PS_LOG_INFO, "  %d kron\n", Nkron);
+
+    psphotVisualShowResidualImage (readout);
+
+    if (doPetrosian) {
+	psphotVisualShowPetrosians (sources);
+    }
+
     return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceFits.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceFits.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotExtendedSourceFits.c	(revision 27840)
@@ -1,6 +1,34 @@
 # include "psphotInternal.h"
 
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotExtendedSourceFits (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    // perform full extended source non-linear fits?
+    if (!psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_FITS")) {
+        psLogMsg ("psphot", PS_LOG_INFO, "skipping extended source measurements\n");
+        return true;
+    }
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotExtendedSourceFitsReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on to fit extended sources for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // non-linear model fitting for extended sources
-bool psphotExtendedSourceFits (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+bool psphotExtendedSourceFitsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
@@ -12,4 +40,22 @@
     bool savePics = false;
 
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping source size");
+	return true;
+    }
+
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
@@ -21,10 +67,4 @@
     // maskVal is used to test for rejected pixels, and must include markVal
     maskVal |= markVal;
-
-    // perform full extended source non-linear fits?
-    if (!psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_FITS")) {
-        psLogMsg ("psphot", PS_LOG_INFO, "skipping extended source measurements\n");
-        return true;
-    }
 
     // select the collection of desired models
@@ -226,5 +266,4 @@
 
           pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-          source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
 
           psFree (modelFluxes);
@@ -253,5 +292,4 @@
         // subtract the best fit from the object, leave local sky
         pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
 
         // the initial model flux is no longer needed
Index: branches/simtest_nebulous_branches/psphot/src/psphotFake.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFake.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFake.c	(revision 27840)
@@ -0,0 +1,220 @@
+# include "psphotInternal.h"
+
+#define TESTING
+
+
+#ifdef TESTING
+#define MIN_FLUX 0.1                    // Minimum flux for faint sources
+#endif
+
+
+// Calculate the limiting magnitude for an image
+//
+// We limit ourselves to calculating the peak flux in the smoothed image, which should be close (modulo
+// non-Gaussian PSF) to the limiting flux in the un-smoothed original image.
+static bool fakeLimit(float *magLim,           // Limiting magntiude, to return
+                      float *minFlux,          // Minimum flux, to return
+                      const pmReadout *ro,     // Readout of interest
+                      float thresh,            // Threshold for source identification
+                      float xFWHM, float yFWHM, // Size of PSF
+                      float smoothNsigma,       // Smoothing limit
+                      psImageMaskType maskVal   // Value to mask
+                      );
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_VARIANCE(ro, false);
+    PS_ASSERT_METADATA_NON_NULL(recipe, false);
+
+    float smoothSigma  = 0.5*(FWHM_X + FWHM_Y) / (2.0*sqrtf(2.0*log(2.0)));
+    psKernel *kernel = psImageSmoothKernel(smoothSigma, smoothNsigma); // Kernel used for smoothing
+    float factor = psImageCovarianceCalculateFactor(kernel, readout->covariance); // Covariance matrix
+    psFree(kernel);
+
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN); // Statistics for variance
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);        // Random number generator
+    if (!psImageBackground(stats, NULL, ro->variance, ro->mask, maskVal, rng) ||
+        !isfinite(stats->robustMedian)) {
+        psError(PSPHOT_ERR_DATA, false, "Unable to determine mean variance");
+        psFree(stats);
+        psFree(rng);
+        return false;
+    }
+    psFree(rng);
+    float meanVar = stats->robustMedian; // Mean variance
+    psFree(stats);
+
+    if (magLim) {
+        *magLim = -2.5 * log10(thresh * sqrtf(meanVar * factor));
+    }
+    if (minFlux) {
+        int fudge = psMetadataLookupF32(NULL, recipe, "FAKE.MINFLUX"); // Fudge factor for minimum flux
+        *minFlux = sqrtf(meanVar) * fudge;
+    }
+
+    return true;
+}
+
+/// Generate a fake image and add it in to the existing readout
+static bool fakeGenerate(psImage **xSrc, psImage **ySrc, // Positions of sources
+                         const pmReadout *ro,            // Readout of interest
+                         const psVector *magOffsets,     // Magnitude offsets for fake sources
+                         int numSources,                 // Number of fake sources for each bin
+                         float refMag,                   // Reference magnitude
+                         float minFlux                   // Minimum flux level for fake image
+                         )
+{
+    PM_ASSERT_READOUT_NON_NULL(ro, false);
+    PM_ASSERT_READOUT_IMAGE(ro, false);
+    PS_ASSERT_METADATA_NON_NULL(recipe, false);
+    PS_ASSERT_VECTOR_NON_NULL(magOffsets, false);
+    PS_ASSERT_VECTOR_TYPE(magOffsets, PS_TYPE_F32, false);
+
+    int numBins = magOffset->n;                                     // Number of bins
+    int numCols = ro->image->numCols, numRows = ro->image->numRows; // Size of image
+
+    *xSrc = psImageRecycle(*xSrc, numSources, numBins, PS_TYPE_F32);
+    *ySrc = psImageRecycle(*ySrc, numSources, numBins, PS_TYPE_F32);
+
+    // Master list, for image creation
+    psVector *xAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+    psVector *yAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+    psVector *magAll = psVectorAlloc(numBins * numSources, PS_TYPE_F32);
+
+    psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS); // Random number generator
+    for (int i = 0; i <= numBins; i++) {
+        psArray *data = psArrayAlloc(3); // Sources for bin
+
+        psVector *x = psVectorAlloc(numSources, PS_TYPE_F32);
+        psVector *y = psVectorAlloc(numSources, PS_TYPE_F32);
+
+        float mag = refMag + magOffset->data.F32[i]; // Instrumental magnitude of sources
+
+        for (int j = 0; j <= numSources; j++) {
+            x->data.F32[j] = psRandomUniform(rng) * numCols;
+            y->data.F32[j] = psRandomUniform(rng) * numRows;
+            magAll->data.F32[i * numSources + j] = mag;
+        }
+        memcpy(xSrc->data.F32[i], x->data.F32, numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+        memcpy(ySrc->data.F32[i], y->data.F32, numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+        memcpy(&xAll->data.F32[i * numSources], x->data.F32, numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+        memcpy(&yAll->data.F32[i * numSources], y->data.F32, numSources * PSELEMTYPE_SIZEOF(PS_TYPE_F32));
+    }
+    psFree(rng);
+
+    pmReadout *fakeRO = pmReadoutAlloc(); // Fake readout
+    if (!pmReadoutFakeFromVectors(fakeRO, xAll, yAll, mag, NULL, NULL, psf, minFlux, NAN, false, false)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate fake image");
+        psFree(fakeRO);
+        psFree(xAll);
+        psFree(yAll);
+        psFree(magAll);
+        return false;
+    }
+    psFree(xAll);
+    psFree(yAll);
+    psFree(magAll);
+
+    psBinaryOp(ro->image, ro->image, "+", fakeRO->image);
+    psFree(fakeRO);
+
+    return true;
+}
+
+
+// *** in this section, perform the photometry for fake sources ***
+bool psphotFake(pmConfig *config, pmReadout *readout, const pmPSF *psf,
+                const psMetadata *recipe, const psArray *realSources)
+{
+    PS_ASSERT_PTR_NON_NULL(config, false);
+    PM_ASSERT_READOUT_NON_NULL(readout, false);
+    PM_ASSERT_READOUT_IMAGE(readout, false);
+    PS_ASSERT_PTR_NON_NULL(psf, false);
+    PS_ASSERT_METADATA_NON_NULL(recipe, false);
+    PS_ASSERT_ARRAY_NON_NULL(realSources, false);
+
+    psTimerStart("psphot.fake");
+
+    // Collect recipe information
+    float xFWHM = psMetadataLookupF32(NULL, recipe, "FWHM_X"); // PSF size in x
+    float yFWHM = psMetadataLookupF32(NULL, recipe, "FWHM_Y"); // PSF size in y
+    if (!isfinite(xFWHM) || !isfinite(yFWHM)) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find FWHM_X and FWHM_Y in recipe");
+        return false;
+    }
+    float smoothNsigma = psMetadataLookupF32(NULL, recipe, "PEAKS_SMOOTH_NSIGMA"); // Smoothing limit
+    if (!isfinite(smoothNsigma)) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find PEAKS_SMOOTH_NSIGMA in recipe");
+        return false;
+    }
+    float thresh = psMetadataLookupF32(NULL, recipe, "PEAKS_NSIGMA_LIMIT_2");
+    if (!isfinite(thresh)) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find PEAKS_NSIGMA_LIMIT_2 in recipe");
+        return false;
+    }
+    psVector *magOffsets = psMetadataLookupVector(NULL, recipe, "FAKE.MAG"); // Magnitude offsets
+    if (!magOffset || magOffset->type.type != PS_TYPE_F32) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find FAKE.MAG F32 vector in recipe");
+        return NULL;
+    }
+    int numSources = psMetadataLookupS32(NULL, recipe, "FAKE.NUM"); // Number of sources for each bin
+    if (numSources == 0) {
+        psError(PSPHOT_ERR_CONFIG, false, "Unable to find FAKE.NUM in recipe");
+        return NULL;
+    }
+    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Value to mask
+
+
+    // remove all sources, adding noise for subtracted sources
+    psphotRemoveAllSources(realSources, recipe);
+    psphotAddNoise(readout, realSources, recipe);
+
+    float magLim;                       // Guess at limiting magnitude
+    float minFlux;                      // Minimum flux for fake image
+    if (!fakeLimit(&magLim, &minFlux, readout, thresh, xFWHM, yFWHM, smoothNsigma, maskVal)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to determine limits for image");
+        return false;
+    }
+
+    psImage *xFake = NULL, *yFake = NULL; // Coordinates of sources, each bin in a row
+    if (!fakeGenerate(&xFake, &yFake, readout, magOffsets, numSources, magLim, minFlux)) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to generate fake sources");
+        psFree(xFake);
+        psFree(yFake);
+        return false;
+    }
+
+    psImage *significance = psphotSignificanceImage(readout, recipe, 2, maskVal);
+    if (!significance) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to generate significance image");
+        psFree(xFake);
+        psFree(yFake);
+        return false;
+    }
+
+    int numBins = magOffsets->n;                          // Number of bins
+    psVector *frac = psVectorAlloc(numBins, PS_TYPE_S32); // Fraction of sources in each bin
+    for (int i = 0; i < numBins; i++) {
+        int numFound = 0;               // Number found
+        for (int j = 0; j < numSources; j++) {
+            float x = xFake->data.F32[i][j], y = yFake->data.F32[i][j]; // Coordinates of interest
+            int xPix = x, yPix = y;                                     // Pixel coordinates
+            if (significance->data.F32[yPix][xPix] > thresh) {
+                numFound++;
+            }
+        }
+        frac->data.S32[i] = (float)numFound / (float)numSources;
+    }
+
+    psFree(xFake);
+    psFree(yFake);
+    psFree(significance);
+
+    // Putting results on recipe because that appears to be the psphot standard, but it's not a good idea
+    psMetadataAddVector(readout->analysis, PS_LIST_TAIL, "FAKE.EFF", PS_META_REPLACE, "Efficiency fractions", frac);
+    psMetadataAddVector(readout->analysis, PS_LIST_TAIL, "FAKE.MAG", PS_META_REPLACE, "Efficiency magnitudes", magOffsets);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "FAKE.REF", PS_META_REPLACE, "Efficiency reference magnitude", magLim);
+
+    psFree(frac);
+
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotFindDetections.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFindDetections.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFindDetections.c	(revision 27840)
@@ -1,6 +1,30 @@
 # include "psphotInternal.h"
 
+// we store the detections on the readout->analysis for each readout this function finds new
+// peaks and new footprints.  any old peaks are saved on oldPeaks.  the resulting footprint set
+// contains all footprints (old and new)
+bool psphotFindDetections (pmConfig *config, const pmFPAview *view, bool firstPass)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotFindDetectionsReadout (config, view, "PSPHOT.INPUT", i, recipe, firstPass)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to find initial detections for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // smooth the image, search for peaks, optionally define footprints based on the peaks
-pmDetections *psphotFindDetections (pmDetections *detections, pmReadout *readout, psMetadata *recipe) {
+bool psphotFindDetectionsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool firstPass) {
 
     bool status;
@@ -9,14 +33,34 @@
     int NMAX = 0;
 
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
-    assert (maskVal);
+    psAssert (maskVal, "missing mask value?");
 
     // Use the new pmFootprints approach?
     const bool useFootprints = psMetadataLookupBool(NULL, recipe, "USE_FOOTPRINTS");
 
-    // on first pass, detections have not yet been allocated
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    // on the initial pass, detections have not yet been allocated or saved on readout->analysis
     if (!detections) {
+	// create the container
         detections = pmDetectionsAlloc();
+	
+	// save detections on the readout->analysis
+	if (!psMetadataAddPtr (readout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_META_REPLACE | PS_DATA_UNKNOWN, "psphot detections", detections)) {
+	    psError (PSPHOT_ERR_CONFIG, false, "problem saving detections on readout");
+	    return false;
+	}
+    } else {
+	psMemIncrRefCounter(detections); // so we can free the detections below
+    }
+
+    if (firstPass) {
         pass = 1;
         NSIGMA_PEAK = psMetadataLookupF32 (&status, recipe, "PEAKS_NSIGMA_LIMIT"); PS_ASSERT (status, NULL);
@@ -30,6 +74,10 @@
     float threshold = PS_SQR(NSIGMA_PEAK);
 
-    // move the old peaks array (if it exists) to oldPeaks
-    // XXX generically, we should be able to call this function an arbitrary number of times
+    // move the old peaks array (if it exists) to oldPeaks XXX generically, we should be able
+    // to call this function an arbitrary number of times the old peaks are saved so they can
+    // be freed later -- the have to be freed after psphotFindFootprints is called below, since
+    // they are also owned by the oldFootprints, which are in turn merged into the new
+    // footprints.  (what about the source->peak entry?)
+    
     assert (detections->oldPeaks == NULL);
     detections->oldPeaks = detections->peaks;
@@ -39,18 +87,28 @@
     psImage *significance = psphotSignificanceImage (readout, recipe, pass, maskVal);
 
-    // display the backsub and backgnd images
-    psphotVisualShowSignificance (significance);
+    // display the significance image
+    psphotVisualShowSignificance (significance, -1.0, PS_SQR(3.0*NSIGMA_PEAK));
+
+    // XXX getting some strange results from significance image
+    if (0) {
+	psImage *lsig = (psImage *) psUnaryOp (NULL, significance, "log");
+	psphotVisualShowSignificance (lsig, 0.0, 4.0);
+	psFree (lsig);
+    }	
 
     // detect the peaks in the significance image
     detections->peaks = psphotFindPeaks (significance, readout, recipe, threshold, NMAX);
-    psMetadataAddF32  (recipe, PS_LIST_TAIL, "PEAK_THRESHOLD", PS_META_REPLACE, "Peak Detection Threshold", threshold);
+    psMetadataAddF32  (readout->analysis, PS_LIST_TAIL, "PEAK_THRESHOLD", PS_META_REPLACE, "Peak Detection Threshold", threshold);
     if (!detections->peaks) {
-        // No peaks found
-        return NULL;
+	// we only get a NULL peaks array due to a programming or config error. 
+	// this will result in a failure.
+	psFree (detections);
+	psError (PSPHOT_ERR_CONFIG, false, "failed on peak search");
+        return false;
     }
 
     // optionally merge peaks into footprints
     if (useFootprints) {
-        psphotFindFootprints (detections, significance, readout, recipe, pass, maskVal);
+	psphotFindFootprints (detections, significance, readout, recipe, threshold, pass, maskVal);
     }
 
@@ -61,5 +119,7 @@
     psphotVisualShowFootprints (detections);
 
-    return detections;
+    psFree (detections);
+
+    return true;
 }
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotFindFootprints.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFindFootprints.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFindFootprints.c	(revision 27840)
@@ -1,5 +1,5 @@
 # include "psphotInternal.h"
 
-bool psphotFindFootprints (pmDetections *detections, psImage *significance, pmReadout *readout, psMetadata *recipe, const int pass, psImageMaskType maskVal) {
+bool psphotFindFootprints (pmDetections *detections, psImage *significance, pmReadout *readout, psMetadata *recipe, const float threshold, const int pass, psImageMaskType maskVal) {
 
     bool status;
@@ -9,17 +9,4 @@
     int npixMin = psMetadataLookupS32(&status, recipe, "FOOTPRINT_NPIXMIN");
     PS_ASSERT (status, false);
-
-    float FOOTPRINT_NSIGMA_LIMIT;
-    if (pass == 1) {
-        FOOTPRINT_NSIGMA_LIMIT = psMetadataLookupS32(&status, recipe, "FOOTPRINT_NSIGMA_LIMIT");
-    } else {
-        FOOTPRINT_NSIGMA_LIMIT = psMetadataLookupS32(&status, recipe, "FOOTPRINT_NSIGMA_LIMIT_2");
-    }
-    PS_ASSERT (status, false);
-
-    // XXX do we need to use the same threshold here as for peaks?  does it make sense for
-    // these to be different?
-
-    float threshold = PS_SQR(FOOTPRINT_NSIGMA_LIMIT);
 
     int growRadius = 0;
@@ -54,7 +41,5 @@
     if (pass == 2) {
         // merge in old peaks;
-        const int includePeaks = 0x0 | 0x2; // i.e. just from newFootprints
-
-        // XXX EAM : still not sure I understand this: are we double-couning or undercounting peaks
+        const int includePeaks = 0x1 | 0x2; // i.e. both new and old footprints
 
         psArray *mergedFootprints = pmFootprintArraysMerge(detections->footprints, footprints, includePeaks);
@@ -63,4 +48,6 @@
         psFree(footprints);
         psFree(detections->footprints);
+
+	// replace the merged footprints on the detection structure
         detections->footprints = mergedFootprints;
     } else {
@@ -68,5 +55,5 @@
     }
 
-    psphotCullPeaks(readout->image, readout->variance, recipe, detections->footprints);
+    psphotCullPeaks(readout, recipe, detections->footprints);
     detections->peaks = pmFootprintArrayToPeaks(detections->footprints);
     psLogMsg ("psphot", PS_LOG_INFO, "%ld peaks, %ld total footprints: %f sec\n", detections->peaks->n, detections->footprints->n, psTimerMark ("psphot.footprints"));
@@ -74,2 +61,17 @@
     return detections;
 }
+
+bool psphotCheckFootprints (pmDetections *detections) {
+
+    // check for messed up footprints in the old peaks
+    for (int i = 0; i < detections->oldPeaks->n; i++) {
+	pmPeak *peak = detections->oldPeaks->data[i];
+	pmFootprint *footprint = peak->footprint;
+	if (!footprint) continue;
+	for (int j = 0; j < footprint->spans->n; j++) {
+	    pmSpan *sp = footprint->spans->data[j];
+	    psAssert (sp, "missing span");
+	}
+    }
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotFindPeaks.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFindPeaks.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFindPeaks.c	(revision 27840)
@@ -13,10 +13,15 @@
     psArray *peaks = pmPeaksInImage (significance, threshold);
     if (peaks == NULL) {
-	// XXX should we be sending back an empty array instead of NULL?
-        // XXX this may also be due to a programming or config error
+	// we only get a NULL peaks array due to a programming or config error. 
+	// this will result in a failure.
+        psError(PSPHOT_ERR_DATA, false, "no peaks found in this image");
+        return NULL;
+    }
+
+    if (peaks->n == 0) {
         // XXX do we need to set something in the readout->analysis to indicate that
         // we tried and failed to find peaks (something in the header data)
-        psError(PSPHOT_ERR_DATA, false, "no peaks found in this image");
-        return false;
+	psLogMsg ("psphot", PS_LOG_INFO, "no peaks found in this image");
+	return peaks;
     }
 
@@ -31,4 +36,10 @@
         peak->SN = sqrt(peak->value);
         peak->flux = readout->image->data.F32[peak->y-row0][peak->x-col0];
+	// if (peak->flux / peak->value > 5.0/12.0) {
+	//     psWarning ("odd peak levels (1)");
+	// }
+	// if (peak->value / peak->flux > 5*12.0) {
+	//     psWarning ("odd peak levels (2)");
+	// }
 	if (readout->variance && isfinite (peak->dx)) {
 	    peak->dx *= sqrt(readout->variance->data.F32[peak->y-row0][peak->x-col0]);
Index: branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinear.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinear.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinear.c	(revision 27840)
@@ -12,5 +12,44 @@
 static bool SetBorderMatrixElements (psSparseBorder *border, pmReadout *readout, psArray *sources, bool constant_weights, int SKY_FIT_ORDER, psImageMaskType markVal);
 
-bool psphotFitSourcesLinear (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotFitSourcesLinear (pmConfig *config, const pmFPAview *view, bool final)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    assert (recipe);
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+
+	// find the currently selected readout
+	pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", i); // File of interest
+	psAssert (file, "missing file?");
+
+	pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+	psAssert (readout, "missing readout?");
+
+	pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+	psAssert (detections, "missing detections?");
+
+	psArray *sources = detections->allSources;
+	psAssert (sources, "missing sources?");
+
+	pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+	psAssert (psf, "missing psf?");
+
+	if (!psphotFitSourcesLinearReadout (recipe, readout, sources, psf, final)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to fit sources (linear) for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotFitSourcesLinearReadout (psMetadata *recipe, pmReadout *readout, psArray *sources, pmPSF *psf, bool final) {
 
     bool status;
@@ -18,5 +57,9 @@
     float y;
     float f;
-    // float r;
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping linear fit");
+	return true;
+    }
 
     psTimerStart ("psphot.linear");
@@ -59,4 +102,11 @@
     }
 
+    // XXX test: choose a larger-than expected radius:
+    float covarFactor = psImageCovarianceFactorForAperture(readout->covariance, 10.0); // Covariance matrix
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "covariance factor: %f\n", covarFactor);
+
+    // XXX do not apply covarFactor for the moment...
+    // covarFactor = 1.0;
+
     // select the sources which will be used for the fitting analysis
     for (int i = 0; i < sources->n; i++) {
@@ -76,5 +126,5 @@
             if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) continue;
         } else {
-            if (source->mode & PM_SOURCE_MODE_BLEND) continue;
+            // if (source->mode & PM_SOURCE_MODE_BLEND) continue;
         }
 
@@ -120,10 +170,10 @@
 
         // diagonal elements of the sparse matrix (auto-cross-product)
-        f = pmSourceModelDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        f = pmSourceModelDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
         psSparseMatrixElement (sparse, i, i, f);
 
         // the formal error depends on the weighting scheme
         if (CONSTANT_PHOTOMETRIC_WEIGHTS) {
-            float var = pmSourceModelDotModel (SRCi, SRCi, false);
+            float var = pmSourceModelDotModel (SRCi, SRCi, false, covarFactor);
             errors->data.F32[i] = 1.0 / sqrt(var);
         } else {
@@ -133,5 +183,5 @@
 
         // find the image x model value
-        f = pmSourceDataDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        f = pmSourceDataDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
         psSparseVectorElement (sparse, i, f);
 
@@ -139,11 +189,11 @@
         switch (SKY_FIT_ORDER) {
           case 1:
-            f = pmSourceModelWeight (SRCi, 1, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            f = pmSourceModelWeight (SRCi, 1, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
             psSparseBorderElementB (border, i, 1, f);
-            f = pmSourceModelWeight (SRCi, 2, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            f = pmSourceModelWeight (SRCi, 2, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
             psSparseBorderElementB (border, i, 2, f);
 
           case 0:
-            f = pmSourceModelWeight (SRCi, 0, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            f = pmSourceModelWeight (SRCi, 0, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
             psSparseBorderElementB (border, i, 0, f);
             break;
@@ -165,5 +215,5 @@
 
             // got an overlap; calculate cross-product and add to output array
-            f = pmSourceModelDotModel (SRCi, SRCj, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            f = pmSourceModelDotModel (SRCi, SRCj, CONSTANT_PHOTOMETRIC_WEIGHTS, covarFactor);
             psSparseMatrixElement (sparse, j, i, f);
         }
@@ -186,5 +236,5 @@
     if (SKY_FIT_LINEAR) {
         psSparseBorderSolve (&norm, &skyfit, constraint, border, 5);
-        fprintf (stderr, "skyfit: %f\n", skyfit->data.F32[0]);
+        psLogMsg ("psphot", PS_LOG_MINUTIA, "skyfit: %f\n", skyfit->data.F32[0]);
     } else {
         norm = psSparseSolve (NULL, constraint, sparse, 5);
@@ -215,14 +265,14 @@
         // subtract object
         pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
     }
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "sub models: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
 
     // measure chisq for each source
-    for (int i = 0; final && (i < fitSources->n); i++) {
+    // for (int i = 0; final && (i < fitSources->n); i++) {
+    for (int i = 0; i < fitSources->n; i++) {
         pmSource *source = fitSources->data[i];
         if (source->mode & PM_SOURCE_MODE_NONLINEAR_FIT) continue;
         pmModel *model = pmSourceGetModel (NULL, source);
-        pmSourceChisq (model, source->pixels, source->maskObj, source->variance, maskVal);
+        pmSourceChisq (model, source->pixels, source->maskObj, source->variance, maskVal, covarFactor);
     }
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "get chisqs: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
@@ -239,5 +289,10 @@
 
     psphotVisualShowResidualImage (readout);
-    psphotVisualShowFlags (sources);
+    psphotVisualPlotChisq (sources);
+    // psphotVisualShowFlags (sources);
+
+    // We have to place this visualization here because the models are not realized until
+    // psphotGuessModels or fitted until psphotFitSourcesLinear.
+    psphotVisualShowPSFStars (recipe, psf, sources);
 
     return true;
@@ -245,4 +300,5 @@
 
 // XXX do we need this?
+// XXX disallow the simultaneous sky fit and remove this code...
 
 // Calculate the weight terms for the sky fit component of the matrix.  This function operates
@@ -264,5 +320,5 @@
         float x = model->params->data.F32[PM_PAR_XPOS];
         float y = model->params->data.F32[PM_PAR_YPOS];
-        psImageMaskCircle (source->maskView, x, y, model->radiusFit, "AND", PS_NOT_IMAGE_MASK(markVal));
+        psImageMaskCircle (source->maskView, x, y, model->fitRadius, "AND", PS_NOT_IMAGE_MASK(markVal));
     }
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinearStack.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinearStack.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotFitSourcesLinearStack.c	(revision 27840)
@@ -1,22 +1,16 @@
 # include "psphotInternal.h"
+// XXX need array of covar factors for each image
+// XXX define the 'good' / 'bad' flags?
 
-// fit flux (and optionally sky model) to all reasonable sources
-// with the linear fitting process.  sources must have an associated
-// model with selected pixels, and the fit radius must be defined
+# define COVAR_FACTOR 1.0
 
-// given the set of sources, each of which points to the pixels in the
-// science image, we construct a set of simulated sources with their own pixels.
-// these are used to determine the simultaneous linear fit of fluxes.
-// the analysis is performed wrt the simulated pixel values
-
-static bool SetBorderMatrixElements (psSparseBorder *border, pmReadout *readout, psArray *sources, bool constant_weights, psImageMaskType markVal);
-
-bool psphotFitSourcesLinear (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, bool final) {
+bool psphotFitSourcesLinearStack (pmConfig *config, psArray *objects, bool final) {
 
     bool status;
-    float x;
-    float y;
     float f;
-    // float r;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    assert (recipe);
 
     psTimerStart ("psphot.linear");
@@ -33,45 +27,44 @@
     maskVal |= markVal;
 
-    // source analysis is done in spatial order
-    sources = psArraySort (sources, pmSourceSortByY);
+    // analysis is done in spatial order (to speed up overlap search)
+    // sort by first element in each source list
+    objects = psArraySort (objects, pmPhotObjSortByX);
 
     // storage array for fitSources
-    psArray *fitSources = psArrayAllocEmpty (sources->n);
+    psArray *fitSources = psArrayAllocEmpty (objects->n);
 
-    bool CONSTANT_PHOTOMETRIC_WEIGHTS =
-        psMetadataLookupBool(&status, recipe, "CONSTANT_PHOTOMETRIC_WEIGHTS");
-    if (!status) {
-        psAbort("You must provide a value for the BOOL recipe CONSTANT_PHOTOMETRIC_WEIGHTS");
-    }
+    bool CONSTANT_PHOTOMETRIC_WEIGHTS = psMetadataLookupBool(&status, recipe, "CONSTANT_PHOTOMETRIC_WEIGHTS");
+    psAssert (status, "You must provide a value for the BOOL recipe CONSTANT_PHOTOMETRIC_WEIGHTS");
+
+    // XXX store a local static array of covar factors for each of the images (by image ID)
+    // float covarFactor = psImageCovarianceFactorForAperture(readout->covariance, 10.0); // Covariance matrix
+    // psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "covariance factor: %f\n", covarFactor);
 
     // select the sources which will be used for the fitting analysis
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
+    for (int i = 0; i < objects->n; i++) {
+        pmPhotObj *object = objects->data[i];
+	if (!object) continue;
+	if (!object->sources) continue;
 
-        // turn this bit off and turn it on again if we pass this test
-        source->mode &= ~PM_SOURCE_MODE_LINEAR_FIT;
+	// XXX check an element of the group to see if we should use it
+	// if (!object->flags & PM_PHOT_OBJ_BAD) continue;
 
-        // skip non-astronomical objects (very likely defects)
-        if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
-        if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
+	for (int j = 0; j < object->sources->n; j++) {
+	  pmSource *source = object->sources->data[j];
+	  if (!source) continue;
 
-        // do not include CRs in the full ensemble fit
-        if (source->mode & PM_SOURCE_MODE_CR_LIMIT) continue;
+	  // turn this bit off and turn it on again if we keep this source
+	  source->mode &= ~PM_SOURCE_MODE_LINEAR_FIT;
 
-        if (final) {
-            if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) continue;
-        } else {
-            if (source->mode & PM_SOURCE_MODE_BLEND) continue;
-        }
+	  // generate model for sources without, or skip if we can't
+	  if (!source->modelFlux) {
+            if (!pmSourceCacheModel (source, maskVal)) continue;
+	  }
 
-        // generate model for sources without, or skip if we can't
-        if (!source->modelFlux) {
-            if (!pmSourceCacheModel (source, maskVal)) continue;
-        }
-
-        source->mode |= PM_SOURCE_MODE_LINEAR_FIT;
-        psArrayAdd (fitSources, 100, source);
+	  source->mode |= PM_SOURCE_MODE_LINEAR_FIT;
+	  psArrayAdd (fitSources, 100, source);
+	}
     }
-    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built fitSources: %f sec (%ld objects)\n", psTimerMark ("psphot.linear"), sources->n);
+    psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built fitSources: %f sec (%ld objects)\n", psTimerMark ("psphot.linear"), objects->n);
 
     if (fitSources->n == 0) {
@@ -83,8 +76,6 @@
     psVector *errors = psVectorAlloc (fitSources->n, PS_TYPE_F32);
 
-    // create the border matrix (includes the sparse matrix)
-    // for just sky: 1 row; for x,y terms: 3 rows
+    // create the sparse matrix
     psSparse *sparse = psSparseAlloc (fitSources->n, 100);
-    psSparseBorder *border = psSparseBorderAlloc (sparse, 1);
 
     // fill out the sparse matrix elements and border elements (B)
@@ -95,10 +86,10 @@
 
         // diagonal elements of the sparse matrix (auto-cross-product)
-        f = pmSourceModelDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        f = pmSourceModelDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS, COVAR_FACTOR);
         psSparseMatrixElement (sparse, i, i, f);
 
         // the formal error depends on the weighting scheme
         if (CONSTANT_PHOTOMETRIC_WEIGHTS) {
-            float var = pmSourceModelDotModel (SRCi, SRCi, false);
+            float var = pmSourceModelDotModel (SRCi, SRCi, false, COVAR_FACTOR);
             errors->data.F32[i] = 1.0 / sqrt(var);
         } else {
@@ -106,11 +97,7 @@
         }
 
-
         // find the image x model value
-        f = pmSourceDataDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS);
+        f = pmSourceDataDotModel (SRCi, SRCi, CONSTANT_PHOTOMETRIC_WEIGHTS, COVAR_FACTOR);
         psSparseVectorElement (sparse, i, f);
-
-	f = pmSourceModelWeight (SRCi, 0, CONSTANT_PHOTOMETRIC_WEIGHTS);
-	psSparseBorderElementB (border, i, 0, f);
 
         // loop over all other stars following this one
@@ -118,12 +105,15 @@
             pmSource *SRCj = fitSources->data[j];
 
+	    // we only need to generate dot terms for source on the same image
+	    if (SRCj->imageID != SRCi->imageID) { continue; }
+
             // skip over disjoint source images, break after last possible overlap
-            if (SRCi->pixels->row0 + SRCi->pixels->numRows < SRCj->pixels->row0) break;
-            if (SRCj->pixels->row0 + SRCj->pixels->numRows < SRCi->pixels->row0) continue;
-            if (SRCi->pixels->col0 + SRCi->pixels->numCols < SRCj->pixels->col0) continue;
-            if (SRCj->pixels->col0 + SRCj->pixels->numCols < SRCi->pixels->col0) continue;
+            if (SRCj->pixels->row0 + SRCj->pixels->numRows < SRCi->pixels->row0) continue;  // source(i) is above source(j)
+            if (SRCi->pixels->row0 + SRCi->pixels->numRows < SRCj->pixels->row0) continue;  // source(i) is below source(j)
+            if (SRCj->pixels->col0 + SRCj->pixels->numCols < SRCi->pixels->col0) continue;  // source(i) is right of source(j)
+            if (SRCi->pixels->col0 + SRCi->pixels->numCols < SRCj->pixels->col0) break;     // source(i) is left of source(j) [no other source(j) can overlap source(i)]
 
             // got an overlap; calculate cross-product and add to output array
-            f = pmSourceModelDotModel (SRCi, SRCj, CONSTANT_PHOTOMETRIC_WEIGHTS);
+            f = pmSourceModelDotModel (SRCi, SRCj, CONSTANT_PHOTOMETRIC_WEIGHTS, COVAR_FACTOR);
             psSparseMatrixElement (sparse, j, i, f);
         }
@@ -133,7 +123,4 @@
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "built matrix: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
 
-    // set the sky, sky_x, sky_y components of border matrix
-    SetBorderMatrixElements (border, readout, fitSources, CONSTANT_PHOTOMETRIC_WEIGHTS, SKY_FIT_ORDER, markVal);
-
     psSparseConstraint constraint;
     constraint.paramMin   = 0.0;
@@ -142,13 +129,7 @@
 
     // solve for normalization terms (need include local sky?)
-    psVector *norm = psSparseSolve (NULL, constraint, sparse, 5);
-
+    psVector *norm = NULL;
+    norm = psSparseSolve (NULL, constraint, sparse, 5);
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "solve matrix: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
-
-    // XXXX **** philosophical question:
-    // we measure bright objects in three passes: 1) linear fit; 2) non-linear fit; 3) linear fit:
-    // should retain the chisq and errors from the intermediate non-linear fit?
-    // the non-linear fit provides better values for the position errors, and for
-    // extended sources, the shape errors
 
     // adjust I0 for fitSources and subtract
@@ -164,18 +145,17 @@
         model->params->data.F32[PM_PAR_I0] = norm->data.F32[i];
         model->dparams->data.F32[PM_PAR_I0] = errors->data.F32[i];
-        // XXX is the value of 'errors' modified by the sky fit?
 
         // subtract object
         pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
     }
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "sub models: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
 
     // measure chisq for each source
-    for (int i = 0; final && (i < fitSources->n); i++) {
+    // for (int i = 0; final && (i < fitSources->n); i++) {
+    for (int i = 0; i < fitSources->n; i++) {
         pmSource *source = fitSources->data[i];
         if (source->mode & PM_SOURCE_MODE_NONLINEAR_FIT) continue;
         pmModel *model = pmSourceGetModel (NULL, source);
-        pmSourceChisq (model, source->pixels, source->maskObj, source->variance, maskVal);
+        pmSourceChisq (model, source->pixels, source->maskObj, source->variance, maskVal, COVAR_FACTOR);
     }
     psLogMsg ("psphot.ensemble", PS_LOG_MINUTIA, "get chisqs: %f sec (%d elements)\n", psTimerMark ("psphot.linear"), sparse->Nelem);
@@ -186,67 +166,22 @@
     psFree (norm);
     psFree (errors);
-    psFree (border);
 
     psLogMsg ("psphot.ensemble", PS_LOG_INFO, "measure ensemble of PSFs: %f sec\n", psTimerMark ("psphot.linear"));
-
-    psphotVisualShowResidualImage (readout);
-    psphotVisualShowFlags (sources);
 
     return true;
 }
 
-// Calculate the weight terms for the sky fit component of the matrix.  This function operates
-// on the pixels which correspond to all of the sources of interest.  These elements fill in
-// the border matrix components in the sparse matrix equation.
-static bool SetBorderMatrixElements (psSparseBorder *border, pmReadout *readout, psArray *sources, bool constant_weights, psImageMaskType markVal) {
+// sort by X (ascending)
+int pmPhotObjSortByX (const void **a, const void **b)
+{
+    pmPhotObj *objA = *(pmPhotObj **)a;
+    pmPhotObj *objB = *(pmPhotObj **)b;
 
-    // generate the image-wide weight terms
-    // turn on MARK for all image pixels
-    psRegion fullArray = psRegionSet (0, 0, 0, 0);
-    fullArray = psRegionForImage (readout->mask, fullArray);
-    psImageMaskRegion (readout->mask, fullArray, "OR", markVal);
+    psF32 fA = objA->x;
+    psF32 fB = objB->x;
 
-    // turn off MARK for all object pixels
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-        pmModel *model = pmSourceGetModel (NULL, source);
-        if (model == NULL) continue;
-        float x = model->params->data.F32[PM_PAR_XPOS];
-        float y = model->params->data.F32[PM_PAR_YPOS];
-        psImageMaskCircle (source->maskView, x, y, model->radiusFit, "AND", PS_NOT_IMAGE_MASK(markVal));
-    }
-
-    // accumulate the image statistics from the masked regions
-    psF32 **image  = readout->image->data.F32;
-    psF32 **variance = readout->variance->data.F32;
-    psImageMaskType  **mask   = readout->mask->data.PS_TYPE_IMAGE_MASK_DATA;
-
-    double w, x, y, x2, xy, y2, xc, yc, wt, f, fo, fx, fy;
-    w = x = y = x2 = xy = y2 = fo = fx = fy = 0;
-
-    int col0 = readout->image->col0;
-    int row0 = readout->image->row0;
-
-    for (int j = 0; j < readout->image->numRows; j++) {
-        for (int i = 0; i < readout->image->numCols; i++) {
-            if (mask[j][i]) continue;
-            if (constant_weights) {
-                wt = 1.0;
-            } else {
-                wt = variance[j][i];
-            }
-            f = image[j][i];
-            w   += 1/wt;
-            fo  += f/wt;
-        }
-    }
-
-    // turn off MARK for all image pixels
-    psImageMaskRegion (readout->mask, fullArray, "AND", PS_NOT_IMAGE_MASK(markVal));
-
-    // set the Border T elements
-    psSparseBorderElementG (border, 0, fo);
-    psSparseBorderElementT (border, 0, 0, w);
-
-    return true;
+    psF32 diff = fA - fB;
+    if (diff > FLT_EPSILON) return (+1);
+    if (diff < FLT_EPSILON) return (-1);
+    return (0);
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotForced.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotForced.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotForced.c	(revision 27840)
@@ -0,0 +1,35 @@
+# include "psphotStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+    pmErrorRegister();                  // register psModule's error codes/messages
+    psphotInit();
+
+    // load command-line arguments, options, and system config data
+    pmConfig *config = psphotForcedArguments (argc, argv);
+    assert(config);
+
+    psphotVersionPrint();
+
+    // load input data (config and images (signal, noise, mask)
+    if (!psphotParseCamera (config)) {
+        psErrorStackPrint(stderr, "Error setting up the camera\n");
+        exit (psphotGetExitStatus());
+    }
+
+    // call psphot for each readout
+    if (!psphotForcedImageLoop (config)) {
+        psErrorStackPrint(stderr, "Error in the psphot image loop\n");
+        exit (psphotGetExitStatus());
+    }
+
+    psLogMsg ("psphot", 3, "complete psphot run: %f sec\n", psTimerMark ("complete"));
+
+    psErrorCode exit_status = psphotGetExitStatus();
+    psphotCleanup (config);
+    exit (exit_status);
+}
+
+// all functions which return to this level must raise one of the top-level error codes if they
+// exit with an error.  these error codes are used to specify the program exit status
Index: branches/simtest_nebulous_branches/psphot/src/psphotForcedArguments.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotForcedArguments.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotForcedArguments.c	(revision 27840)
@@ -0,0 +1,199 @@
+# include "psphotStandAlone.h"
+
+static void writeHelpInfo(const char* program, pmConfig* config, FILE* ofile)
+{
+  fprintf(ofile,
+	  "Usage: one of the following\n"
+	  "%s -file fname1[,fname2,...] -mask maskfile1[,maskfile2,...]\n"
+	  "     -variance varfile1[,varfile2,...] OutFileBaseName\n"
+	  "\n"
+	  "%s -list FileNameList [-masklist MaskFileNameList] \n"
+	  "     -variancelist VarFileNameList OutFileBaseName\n"
+	  "\n"
+	  "%s -help\n"
+	  "\n"
+	  "%s -version\n"
+	  "\n"
+	  "where:\n"
+	  "  FileNameList is a text file containing filenames, one per line\n"
+	  "  MaskFileNameList is a text file of mask filenames, one per line\n"
+	  "  VarFileNameList is a text file of variance filenames, one per line\n"
+	  "  OutFileBaseName is the 'root name' for output files\n"
+	  "\n"
+	  "additional options:\n"
+	  "  -psf PsfFile1[,PsfFile2,...] or -psflist PsfFileNameList\n"
+	  "     specify PSF rather than letting %s estimate it\n"
+	  "  -src SrcFile1[,SrcFile2,...] or -srclist SrcFileNameList\n"
+	  "     specify additional sources for PSF generation\n"
+	  "  -chip nn[,nn,...]\n"
+	  "     select detector chips to process; default is all.\n"
+	  "     Indices correspond to zero-based offset in the FPA metadata table.\n"
+	  "  -photcode PhotoCodeName\n"
+	  "     specify photocode\n"
+	  "  -region RegionString\n"
+	  "     specify analysis region.  String is of form '[x0:x1,y0:y1]'\n"
+	  "     To use this option you must define a default in psphot.config\n"
+	  "  -visual\n"
+	  "     turns on interactive display mode\n"
+	  "  -dumpconfig CfgFileName\n"
+          "     causes config info to be dumped to the named file.\n"
+	  "  -break NOTHING|BACKMDL|PEAKS|MOMENTS|PSFMODEL|ENSEMBLE|PASS1\n"
+	  "     choose a point at which to exit processing early\n"
+	  "  -nthreads n\n"
+	  "     set number of parallel threads of execution\n"
+	  "  -F OldFileRule ReplacementFileRule\n"
+	  "     change file naming rule; e.g. '-F PSPHOT.OUTPUT PSPHOT.OUT.CMF.MEF'\n"
+	  "  -D name stringval\n"
+	  "     set a string-valued config parameter\n"
+	  "  -Di name intval\n"
+	  "     set an integer-valued config parameter\n"
+	  "  -Df name fval\n"
+	  "     set a float-valued config parameter\n"
+	  "  -Db name boolval\n"
+	  "     set a boolean-valued config parameter\n"
+	  "  -v, -vv, -vvv\n"
+	  "     set increasing levels of verbosity\n"
+	  "  -logfmt FormatString\n"
+	  "     set format string used for log messages\n"
+	  "  -trace Fac Lvl\n"
+	  "     set tracing for facility Fac to integer Lvl, e.g. '-trace err 10'\n"
+	  "  -trace-levels\n"
+	  "     print current trace levels\n",
+	  program,program,program,program,program);
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+    exit(PS_EXIT_SUCCESS);
+}
+
+static void usage(const char *program,  // Name of the program
+                  psMetadata *arguments, // Command-line arguments
+                  pmConfig *config,      // Configuration
+		  int exitCode
+		  ) 
+{
+  fprintf(stderr,
+	  "Usage: one of the following\n"
+	  "%s -file fname1[,fname2,...] -mask maskfile1[,maskfile2,...]\n"
+	  "     -variance varfile1[,varfile2,...] OutFileBaseName\n"
+	  "\n"
+	  "%s -list FileNameList [-masklist MaskFileNameList] \n"
+	  "     -variancelist VarFileNameList OutFileBaseName\n"
+	  "\n"
+	  "Try '%s -help' for more options and explanation\n",
+	  program,program,program);
+    if (exitCode != PS_EXIT_SUCCESS)
+      psErrorStackPrint(stderr, "Error reading arguments\n");
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+    exit(exitCode);
+}
+
+pmConfig *psphotForcedArguments(int argc, char **argv) {
+
+    int N;
+    bool status, status1, status2;
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSPHOT_RECIPE);
+    if (config == NULL) {
+      psErrorStackPrint(stderr, "Can't read site configuration");
+	exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    PSARGUMENTS_INSTANTIATE_GENERICS( psphot, config, argc, argv );
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSPHOT recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSPHOT_RECIPE);
+
+    // Number of threads is handled
+    PSARGUMENTS_INSTANTIATE_THREADSARG( psphot, config, argc, argv )
+
+    // visual : interactive display mode
+    if ((N = psArgumentGet (argc, argv, "-visual"))) {
+        psArgumentRemove (N, &argc, argv);
+        pmVisualSetVisual(true);
+    }
+
+    // break : used from recipe throughout psphotReadout
+    if ((N = psArgumentGet (argc, argv, "-break"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "BREAK_POINT", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // analysis region : overrides recipe value, used in psphotReadout/psphotEnsemblePSF
+    if ((N = psArgumentGet (argc, argv, "-region"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "ANALYSIS_REGION", 0, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_DATA_STRING, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // if these command-line options are supplied, load the file name lists into config->arguments
+    // override any configuration-specified source for these files
+    //
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "MASK",       "-mask",     "-masklist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "VARIANCE",   "-variance", "-variancelist");
+
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "PSPHOT.PSF", "-psf",      "-psflist");
+    if (!status) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "No psf model is supplied (use -psf)");
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    status1 = pmConfigFileSetsMD (config->arguments, &argc, argv, "SRC", "-src", "-srclist");
+    status2 = pmConfigFileSetsMD (config->arguments, &argc, argv, "SRCTEXT", "-srctext", "-srctextlist");
+    
+    if (!status1 && !status2) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "No source list is supplied (use one of -src, -srctext, -srclist, or -srctextlist)");
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    if (argc == 1) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Too few arguments: %d", argc);
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    if (psArgumentGet(argc, argv, "-help") ||
+	psArgumentGet(argc, argv, "-h"))
+      writeHelpInfo(argv[0], config, stdout);
+      
+    // the input file is a required argument; if not found, we will exit
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSPHOT_ERR_ARGUMENTS, false, "pmConfigFileSetsMD failed to parse arguments");
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    if (argc != 2) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Expected to see one more argument; saw %d", argc - 1);
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    // output position is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psphot", 1, "Done with psphotForcedArguments...\n");
+    return (config);
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotForcedImageLoop.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotForcedImageLoop.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotForcedImageLoop.c	(revision 27840)
@@ -0,0 +1,129 @@
+# include "psphotStandAlone.h"
+
+# define ESCAPE(MESSAGE) { \
+  psError(PSPHOT_ERR_DATA, false, MESSAGE); \
+  psFree (view); \
+  return false; \
+}
+
+bool psphotForcedImageLoop (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    pmFPAfile *load = psMetadataLookupPtr (&status, config->files, "PSPHOT.LOAD");
+    if (!status) {
+        psError(PSPHOT_ERR_PROG, false, "Can't find input data!");
+        return false;
+    }
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSPHOT.INPUT");
+    if (!status) {
+        psError(PSPHOT_ERR_PROG, false, "Can't find input data!");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmHDU *lastHDU = NULL;              // Last HDU updated
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for fpa in psphot.");
+
+    // for psphot, we force data to be read at the chip level
+    while ((chip = pmFPAviewNextChip (view, load->fpa, 1)) != NULL) {
+        psLogMsg ("psphot", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (! chip->process || ! chip->file_exists) { continue; }
+
+        // load just the input image data (image, mask, weight)
+        pmFPAfileActivate (config->files, false, NULL);
+        pmFPAfileActivate (config->files, true, "PSPHOT.LOAD");
+        pmFPAfileActivate (config->files, true, "PSPHOT.MASK");
+        pmFPAfileActivate (config->files, true, "PSPHOT.VARIANCE");
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for Chip in psphot.");
+
+        // mosaic the cells of a chip into a single contiguous (trimmed) chip
+        if (!psphotMosaicChip(config, view, "PSPHOT.INPUT", "PSPHOT.LOAD")) ESCAPE ("Unable to mosaic chip.");
+
+        // try to load other supporting data (PSF, SRC, etc).
+        // do not re-load the following three files
+        pmFPAfileActivate (config->files, true, NULL);
+        pmFPAfileActivate (config->files, false, "PSPHOT.LOAD");
+        pmFPAfileActivate (config->files, false, "PSPHOT.MASK");
+        pmFPAfileActivate (config->files, false, "PSPHOT.VARIANCE");
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for Chip in psphot.");
+
+        // re-activate files so they will be closed and freed below
+        pmFPAfileActivate (config->files, true, NULL);
+
+        // there is now only a single chip (multiple readouts?). loop over it and process
+        while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psLogMsg ("psphot", 5, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+                psLogMsg ("psphot", 6, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+                if (! readout->data_exists) { continue; }
+
+                // Update the header
+		pmHDU *hdu = pmHDUGetHighest(input->fpa, chip, cell);
+		if (hdu && hdu != lastHDU) {
+		    psphotVersionHeaderFull(hdu->header);
+		    lastHDU = hdu;
+                }
+
+		// if an external mask is supplied, ensure that NAN pixels are also masked
+		if (readout->mask) {
+		    psImageMaskType maskSat = pmConfigMaskGet("SAT", config); // Mask value for saturated pixels
+		    if (!pmReadoutMaskNonfinite(readout, maskSat)) {
+			psError(psErrorCodeLast(), false, "Unable to mask non-finite pixels.");
+			psFree(view);
+			return false;
+		    }
+		}
+
+                // run the actual photometry analysis on this chip/cell/readout
+                if (!psphotForcedReadout (config, view)) {
+                    psError(psErrorCodeLast(), false, "failure in psphotReadout for chip %d, cell %d, readout %d\n", view->chip, view->cell, view->readout);
+                    psFree (view);
+                    return false;
+                }
+            }
+
+            status = true;
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL");
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL.STDEV");
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKGND");
+            if (!status) {
+                psError(PSPHOT_ERR_PROG, false, "trouble dropping internal files");
+                psFree (view);
+                return false;
+            }
+        }
+
+        // save output which is saved at the chip level
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed output for Chip in psphot.");
+    }
+    // save output which is saved at the fpa level
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed ouput for FPA in psphot.");
+
+    // fail if we failed to handle an error
+    if (psErrorCodeLast() != PS_ERR_NONE) psAbort ("failed to handle an error!");
+
+    psFree (view);
+    return true;
+}
+
+// I/O files related to psphot:
+// PSPHOT.INPUT   : input image file(s)
+// PSPHOT.RESID   : residual image
+// PSPHOT.OUTPUT  : output object tables (object)
+
+// PSPHOT.BACKSUB : background subtracted image
+// PSPHOT.BACKGND : background model (full-scale image?)
+// PSPHOT.BACKMDL : background model (binned image?)
+// PSPHOT.PSF     : sample PSF images
+
+// PSPHOT.MASK
+// PSPHOT.VARIANCE
+//
Index: branches/simtest_nebulous_branches/psphot/src/psphotForcedReadout.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotForcedReadout.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotForcedReadout.c	(revision 27840)
@@ -0,0 +1,89 @@
+# include "psphotInternal.h"
+
+bool psphotForcedReadout(pmConfig *config, const pmFPAview *view) {
+
+    // measure the total elapsed time in psphotReadout.  XXX the current threading plan
+    // for psphot envisions threading within psphotReadout, not multiple threads calling
+    // the same psphotReadout.  In the current plan, this dtime is the elapsed time used
+    // jointly by the multiple threads, not the total time used by all threads.
+    psTimerStart ("psphotReadout");
+
+    // allow objects to be fit with ugly models (central holes, extreme asymmetry, etc)
+    pmModelClassSetLimits(PM_MODEL_LIMITS_LAX);
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
+    if (!recipe) {
+        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
+        return false;
+    }
+
+    // set the photcode for this image
+    if (!psphotAddPhotcode (config, view)) {
+        psError (PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
+        return false;
+    }
+
+    // optional break-point for processing
+    char *breakPt = psMetadataLookupStr (NULL, recipe, "BREAK_POINT");
+    PS_ASSERT_PTR_NON_NULL (breakPt, false);
+
+    // Generate the mask and weight images, including the user-defined analysis region of interest
+    psphotSetMaskAndVariance (config, view);
+    if (!strcasecmp (breakPt, "NOTHING")) {
+        return psphotReadoutCleanup(config, view);
+    }
+
+    // generate a background model (median, smoothed image)
+    if (!psphotModelBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!psphotSubtractBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "BACKMDL")) {
+        return psphotReadoutCleanup (config, view);
+    }
+
+    if (!psphotLoadPSF (config, view)) {
+    	// this only happens if we had a programming error in psphotLoadPSF
+        psError (PSPHOT_ERR_UNKNOWN, false, "error loading psf model");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // include externally-supplied sources
+    psphotLoadExtSources (config, view);
+
+    // construct an initial model for each object, set the radius to fitRadius, set circular fit mask
+    psphotGuessModels (config, view);
+
+    // merge the newly selected sources into the existing list
+    // NOTE: merge OLD and NEW
+    psphotMergeSources (config, view);
+
+    // linear PSF fit to source peaks, subtract the models from the image (in PSF mask)
+    psphotFitSourcesLinear (config, view, false);
+
+    // identify CRs and extended sources
+    // XXX do I want to do this step?
+    // XXX do I want to / need to calculate the moments?
+    // psphotSourceSize (config, readout, sources, recipe, psf, 0);
+
+    // calculate source magnitudes
+    psphotMagnitudes(config, view);
+
+    // XXX do I want to do this?
+    // if (!psphotEfficiency(config, readout, view, psf, recipe, sources)) {
+    //     psErrorStackPrint(stderr, "Unable to determine detection efficiencies from fake sources");
+    //     psErrorClear();
+    // }
+
+    // replace background in residual image
+    psphotSkyReplace (config, view);
+
+    // drop the references to the image pixels held by each source
+    psphotSourceFreePixels (config, view);
+
+    // create the exported-metadata and free local data
+    return psphotReadoutCleanup(config, view);
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotGuessModels.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotGuessModels.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotGuessModels.c	(revision 27840)
@@ -3,14 +3,58 @@
 // XXX : the threading here is not great.  this may be due to blocks between elements, but
 // the selection of the objects in a cell is not optimal.  To fix:
-// 1) define the boundaries of the cells up front 
+// 1) define the boundaries of the cells up front
 // 2) loop over the sources once and associate them with their cell
 // 3) define the threaded function to work with sources for a given cell
 
-// construct an initial PSF model for each object
-bool psphotGuessModels (pmConfig *config, pmReadout *readout, psArray *sources, pmPSF *psf) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotGuessModels (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+        if (!psphotGuessModelsReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on to guess models for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
+
+// construct an initial PSF model for each object (new sources only)
+bool psphotGuessModelsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index) {
 
     bool status;
 
     psTimerStart ("psphot.models");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+        psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping model guess");
+        return true;
+    }
+
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    psAssert (psf, "missing psf?");
 
     // select the appropriate recipe information
@@ -21,5 +65,5 @@
     int nThreads = psMetadataLookupS32(&status, config->arguments, "NTHREADS"); // Number of threads
     if (!status) {
-	nThreads = 0;
+        nThreads = 0;
     }
 
@@ -36,5 +80,5 @@
 
     // setup the PSF fit radius details
-    psphotInitRadiusPSF (recipe, psf->type);
+    psphotInitRadiusPSF (recipe, readout->analysis, psf->type);
 
     // choose Cx, Cy (see psphotThreadTools.c for overview of the concepts)
@@ -46,43 +90,43 @@
     for (int i = 0; i < cellGroups->n; i++) {
 
-	psArray *cells = cellGroups->data[i];
-
-	for (int j = 0; j < cells->n; j++) {
-
-	    // allocate a job -- if threads are not defined, this just runs the job
-	    psThreadJob *job = psThreadJobAlloc ("PSPHOT_GUESS_MODEL");
-	    psArrayAdd(job->args, 1, readout);
-	    psArrayAdd(job->args, 1, cells->data[j]); // sources
-	    psArrayAdd(job->args, 1, psf);
-
-	    // XXX change these to use abstract mask type info
-	    PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
-	    PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
-
-	    if (!psThreadJobAddPending(job)) {
-		psError(PS_ERR_UNKNOWN, false, "Unable to guess model.");
-		psFree (job);
-		return false;
-	    }
-	    psFree(job);
-	}
-
-	// wait for the threads to finish and manage results
-	// wait here for the threaded jobs to finish
-	// fprintf (stderr, "wait for threads (%d, %d)\n", jx, jy);
-	if (!psThreadPoolWait (false)) {
-	    psError(PS_ERR_UNKNOWN, false, "Unable to guess model.");
-	    return false;
-	}
-
-	// we have only supplied one type of job, so we can assume the types here
-	psThreadJob *job = NULL;
-	while ((job = psThreadJobGetDone()) != NULL) {
-	    // we have no returned data from this operation
-	    if (job->args->n < 1) {
-		fprintf (stderr, "error with job\n");
-	    }
-	    psFree(job);
-	}
+        psArray *cells = cellGroups->data[i];
+
+        for (int j = 0; j < cells->n; j++) {
+
+            // allocate a job -- if threads are not defined, this just runs the job
+            psThreadJob *job = psThreadJobAlloc ("PSPHOT_GUESS_MODEL");
+            psArrayAdd(job->args, 1, readout);
+            psArrayAdd(job->args, 1, cells->data[j]); // sources
+            psArrayAdd(job->args, 1, psf);
+
+            // XXX change these to use abstract mask type info
+            PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
+            PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
+
+            if (!psThreadJobAddPending(job)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to guess model.");
+                psFree (job);
+                return false;
+            }
+            psFree(job);
+        }
+
+        // wait for the threads to finish and manage results
+        // wait here for the threaded jobs to finish
+        // fprintf (stderr, "wait for threads (%d, %d)\n", jx, jy);
+        if (!psThreadPoolWait (false)) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to guess model.");
+            return false;
+        }
+
+        // we have only supplied one type of job, so we can assume the types here
+        psThreadJob *job = NULL;
+        while ((job = psThreadJobGetDone()) != NULL) {
+            // we have no returned data from this operation
+            if (job->args->n < 1) {
+                fprintf (stderr, "error with job\n");
+            }
+            psFree(job);
+        }
     }
 
@@ -90,9 +134,9 @@
     int nMiss = 0;
     for (int i = 0; i < sources->n; i++) {
-	pmSource *source = sources->data[i];
-	if (source->tmpFlags & PM_SOURCE_TMPF_MODEL_GUESS) {
-	    continue;
-	}
-	nMiss ++;
+        pmSource *source = sources->data[i];
+        if (source->tmpFlags & PM_SOURCE_TMPF_MODEL_GUESS) {
+            continue;
+        }
+        nMiss ++;
     }
     psAssert (nMiss == 0, "failed to attempt to build models for %d objects\n", nMiss);
@@ -110,5 +154,5 @@
     psArray *sources   = job->args->data[1];
     pmPSF *psf         = job->args->data[2];
-    
+
     psImageMaskType maskVal = PS_SCALAR_VALUE(job->args->data[3],PS_TYPE_IMAGE_MASK_DATA);
     psImageMaskType markVal = PS_SCALAR_VALUE(job->args->data[4],PS_TYPE_IMAGE_MASK_DATA);
@@ -117,69 +161,69 @@
 
     for (int i = 0; i < sources->n; i++) {
-	pmSource *source = sources->data[i];
-
-	// this is used to mark sources for which the model is measured. We check later that
-	// all are used.
-	source->tmpFlags |= PM_SOURCE_TMPF_MODEL_GUESS;
-
-	// skip non-astronomical objects (very likely defects)
-	if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
-	if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
-	if (!source->peak) continue;
-
-	nSrc ++;
-	
-	// the guess central intensity comes from the peak:
-	float Io = source->peak->flux;
-
-	// We have two options to get a guess for the object position: the position from the
-	// peak and the position from the moments.  Use the peak position if (a) there are no
-	// moments and (b) the sources is not saturated
-
-	bool useMoments = false;
-	useMoments = (source->mode & PM_SOURCE_MODE_SATSTAR);  // we only want to try if SATSTAR is set, but..
-	useMoments = (useMoments && source->moments);	       // can't if there are no moments
-	useMoments = (useMoments && source->moments->nPixels); // can't if the moments were not measured
-	useMoments = (useMoments && !(source->mode && PM_SOURCE_MODE_MOMENTS_FAILURE)); // can't if the moments failed...
-
-	float Xo, Yo;
-	if (useMoments) {
-	    Xo = source->moments->Mx;
-	    Yo = source->moments->My;
-	} else {
-	    Xo = source->peak->xf;
-	    Yo = source->peak->yf;
-	}
-
-	// set PSF parameters for this model (apply 2D shape model to coordinates Xo, Yo)
-	pmModel *modelPSF = pmModelFromPSFforXY(psf, Xo, Yo, Io);
-
-	if (modelPSF == NULL) {
-	    psWarning ("Failed to determine PSF model at (%f,%f); trying image center", Xo, Yo);
-
-	    float Xc = 0.5*readout->image->numCols;
-	    float Yc = 0.5*readout->image->numRows;
-	    pmModel *modelPSF = pmModelFromPSFforXY(psf, Xc, Yc, Io);
-	    if (modelPSF == NULL) {
-		psError(PSPHOT_ERR_PSF, false, "Failed to determine PSF model at center of image");
-		return false;
-	    }
-
-	    // Now set the object position at the expected location:
-	    modelPSF->params->data.F32[PM_PAR_XPOS] = Xo;
-	    modelPSF->params->data.F32[PM_PAR_YPOS] = Yo;
-	    source->mode |= PM_SOURCE_MODE_BADPSF;
-	}
-
-	// set the fit radius based on the object flux limit and the model
-	// this function affects the mask pixels
-	psphotCheckRadiusPSF (readout, source, modelPSF, markVal);
-
-	// set the source PSF model
-	source->modelPSF = modelPSF;
-	source->modelPSF->residuals = psf->residuals;
-
-	pmSourceCacheModel (source, maskVal);  // ALLOC x14 (!)
-
+        pmSource *source = sources->data[i];
+
+        // this is used to mark sources for which the model is measured. We check later that
+        // all are used.
+        source->tmpFlags |= PM_SOURCE_TMPF_MODEL_GUESS;
+
+        // skip non-astronomical objects (very likely defects)
+        if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
+        if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
+        if (!source->peak) continue;
+
+        nSrc ++;
+
+        // the guess central intensity comes from the peak:
+        float Io = source->peak->flux;
+
+        // We have two options to get a guess for the object position: the position from the
+        // peak and the position from the moments.  Use the peak position if (a) there are no
+        // moments and (b) the sources is not saturated
+
+        bool useMoments = false;
+        useMoments = (source->mode & PM_SOURCE_MODE_SATSTAR);  // we only want to try if SATSTAR is set, but..
+        useMoments = (useMoments && source->moments);          // can't if there are no moments
+        useMoments = (useMoments && source->moments->nPixels); // can't if the moments were not measured
+        useMoments = (useMoments && !(source->mode && PM_SOURCE_MODE_MOMENTS_FAILURE)); // can't if the moments failed...
+
+        float Xo, Yo;
+        if (useMoments) {
+            Xo = source->moments->Mx;
+            Yo = source->moments->My;
+        } else {
+            Xo = source->peak->xf;
+            Yo = source->peak->yf;
+        }
+
+        // set PSF parameters for this model (apply 2D shape model to coordinates Xo, Yo)
+        pmModel *modelPSF = pmModelFromPSFforXY(psf, Xo, Yo, Io);
+
+        if (modelPSF == NULL) {
+            psWarning ("Failed to determine PSF model at (%f,%f); trying image center", Xo, Yo);
+
+            float Xc = 0.5*readout->image->numCols;
+            float Yc = 0.5*readout->image->numRows;
+            pmModel *modelPSF = pmModelFromPSFforXY(psf, Xc, Yc, Io);
+            if (modelPSF == NULL) {
+                psError(PSPHOT_ERR_PSF, false, "Failed to determine PSF model at center of image");
+                return false;
+            }
+
+            // Now set the object position at the expected location:
+            modelPSF->params->data.F32[PM_PAR_XPOS] = Xo;
+            modelPSF->params->data.F32[PM_PAR_YPOS] = Yo;
+            source->mode |= PM_SOURCE_MODE_BADPSF;
+        }
+
+        // set the fit radius based on the object flux limit and the model
+        // this function affects the mask pixels
+        psphotCheckRadiusPSF (readout, source, modelPSF, markVal);
+
+        // set the source PSF model
+        psAssert (source->modelPSF == NULL, "failed to free one of the models?");
+        source->modelPSF = modelPSF;
+        source->modelPSF->residuals = psf->residuals;
+
+        pmSourceCacheModel (source, maskVal);  // ALLOC x14 (!)
     }
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotImageLoop.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotImageLoop.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotImageLoop.c	(revision 27840)
@@ -67,18 +67,19 @@
 
                 // Update the header
-                {
-                    pmHDU *hdu = pmHDUGetHighest(input->fpa, chip, cell);
-                    if (hdu && hdu != lastHDU) {
-                        psphotVersionHeaderFull(hdu->header);
-                        lastHDU = hdu;
-                    }
+		pmHDU *hdu = pmHDUGetHighest(input->fpa, chip, cell);
+		if (hdu && hdu != lastHDU) {
+		    psphotVersionHeaderFull(hdu->header);
+		    lastHDU = hdu;
                 }
 
-                psImageMaskType maskSat = pmConfigMaskGet("SAT", config); // Mask value for saturated pixels
-                if (!pmReadoutMaskNonfinite(readout, maskSat)) {
-                    psError(psErrorCodeLast(), false, "Unable to mask non-finite pixels.");
-                    psFree(view);
-                    return false;
-                }
+		// if an external mask is supplied, ensure that NAN pixels are also masked
+		if (readout->mask) {
+		    psImageMaskType maskSat = pmConfigMaskGet("SAT", config); // Mask value for saturated pixels
+		    if (!pmReadoutMaskNonfinite(readout, maskSat)) {
+			psError(psErrorCodeLast(), false, "Unable to mask non-finite pixels.");
+			psFree(view);
+			return false;
+		    }
+		}
 
                 // run the actual photometry analysis on this chip/cell/readout
@@ -90,15 +91,15 @@
             }
 
-            status = true;
-            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL");
-            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL.STDEV");
-            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKGND");
-            if (!status) {
-                psError(PSPHOT_ERR_PROG, false, "trouble dropping internal files");
-                psFree (view);
-                return false;
-            }
+	    // drop all versions of the internal files
+	    status = true;
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL");
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL.STDEV");
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKGND");
+	    if (!status) {
+		psError(PSPHOT_ERR_PROG, false, "trouble dropping internal files");
+		psFree (view);
+		return false;
+	    }
         }
-
         // save output which is saved at the chip level
         if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed output for Chip in psphot.");
Index: branches/simtest_nebulous_branches/psphot/src/psphotImageQuality.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotImageQuality.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotImageQuality.c	(revision 27840)
@@ -4,7 +4,59 @@
 // XXX Lots of code duplication here
 
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotImageQuality (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+	if (!psphotImageQualityReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on to measure image quality for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // selecting the 'good' stars (likely to be psf stars), measure the M_cn, M_sn terms for n = 2,3,4
-bool psphotImageQuality(psMetadata *recipe, psArray *sources)
+bool psphotImageQualityReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe)
 {
+    bool status = true;
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    // if we have a PSF, skip this analysis
+    if (psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF")) {
+	return true;
+    }
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping image quality");
+	return true;
+    }
+
     psVector *FWHM_MAJOR = psVectorAllocEmpty(100, PS_TYPE_F32);
     psVector *FWHM_MINOR = psVectorAllocEmpty(100, PS_TYPE_F32);
@@ -81,10 +133,9 @@
 
     if (num == 0) {
-      psWarning("Unable to find sources from which to measure image quality");
-      return false;
-    }
-
-    psMetadataAddS32(recipe, PS_LIST_TAIL, "IQ_NSTAR", PS_META_REPLACE,
-                     "Number of stars used for IQ measurements", M2->n);
+	psLogMsg ("psphot", PS_LOG_INFO, "no valid sources for image quality, skipping");
+	return true;
+    }
+
+    psMetadataAddS32(readout->analysis, PS_LIST_TAIL, "IQ_NSTAR", PS_META_REPLACE, "Number of stars used for IQ measurements", M2->n);
 
 // XXX make this a recipe option
@@ -98,8 +149,6 @@
 
     float fwhm_major = stats->sampleMean;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW1",   PS_META_REPLACE,
-                     "FWHM of Major Axis from moments", stats->sampleMean);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW1_E", PS_META_REPLACE,
-                     "FWHM scatter (Major) from moments", stats->sampleStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW1",   PS_META_REPLACE, "FWHM of Major Axis from moments", stats->sampleMean);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW1_E", PS_META_REPLACE, "FWHM scatter (Major) from moments", stats->sampleStdev);
 
     if (!psVectorStats(stats, FWHM_MINOR, NULL, NULL, 0)) {
@@ -108,8 +157,6 @@
     }
     float fwhm_minor = stats->sampleMean;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW2",   PS_META_REPLACE,
-                     "FWHM of Minor Axis from moments", stats->sampleMean);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW2_E", PS_META_REPLACE,
-                     "FWHM scatter (Minor) from moments", stats->sampleStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW2",   PS_META_REPLACE, "FWHM of Minor Axis from moments", stats->sampleMean);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW2_E", PS_META_REPLACE, "FWHM scatter (Minor) from moments", stats->sampleStdev);
 
     if (!psVectorStats(stats, M2, NULL, NULL, 0)) {
@@ -119,12 +166,8 @@
     float vM2 = stats->sampleMean;
     float dM2 = stats->sampleStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2",    PS_META_REPLACE,
-                     "M_2 = sqrt (M_c2^2 + M_s2^2)", vM2);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_ER", PS_META_REPLACE,
-                     "Stdev of M_2", dM2);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_2", stats->sampleLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_2", stats->sampleUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2",    PS_META_REPLACE, "M_2 = sqrt (M_c2^2 + M_s2^2)", vM2);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_ER", PS_META_REPLACE, "Stdev of M_2", dM2);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_LQ", PS_META_REPLACE, "Lower Quartile of M_2", stats->sampleLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_UQ", PS_META_REPLACE, "Upper Quartile of M_2", stats->sampleUQ);
 
     if (!psVectorStats(stats, M2c, NULL, NULL, 0)) {
@@ -132,12 +175,8 @@
         goto FAIL;
     }
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C",   PS_META_REPLACE,
-                     "M_2c = sum f r^2 cos(2phi) / sum f", stats->sampleMean);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_E", PS_META_REPLACE,
-                     "Stdev of M_2c", stats->sampleStdev);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_L", PS_META_REPLACE,
-                     "Lower Quartile of M_2c", stats->sampleLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_U", PS_META_REPLACE,
-                     "Upper Quartile of M_2c", stats->sampleUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C",   PS_META_REPLACE, "M_2c = sum f r^2 cos(2phi) / sum f", stats->sampleMean);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_E", PS_META_REPLACE, "Stdev of M_2c", stats->sampleStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_L", PS_META_REPLACE, "Lower Quartile of M_2c", stats->sampleLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_U", PS_META_REPLACE, "Upper Quartile of M_2c", stats->sampleUQ);
 
     if (!psVectorStats(stats, M2s, NULL, NULL, 0)) {
@@ -145,12 +184,8 @@
         goto FAIL;
     }
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S",   PS_META_REPLACE,
-                     "M_2s = sum f r^2 cos(2phi) / sum f", stats->sampleMean);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_E", PS_META_REPLACE,
-                     "Stdev of M_2s", stats->sampleStdev);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_L", PS_META_REPLACE,
-                     "Lower Quartile of M_2s", stats->sampleLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_U", PS_META_REPLACE,
-                     "Upper Quartile of M_2s", stats->sampleUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S",   PS_META_REPLACE, "M_2s = sum f r^2 cos(2phi) / sum f", stats->sampleMean);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_E", PS_META_REPLACE, "Stdev of M_2s", stats->sampleStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_L", PS_META_REPLACE, "Lower Quartile of M_2s", stats->sampleLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_U", PS_META_REPLACE, "Upper Quartile of M_2s", stats->sampleUQ);
 
     if (!psVectorStats(stats, M3, NULL, NULL, 0)) {
@@ -160,12 +195,8 @@
     float vM3 = stats->sampleMean;
     float dM3 = stats->sampleStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3",   PS_META_REPLACE,
-                     "M_3 = sqrt (M_c3^2 + M_s3^2)", vM3);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_ER", PS_META_REPLACE,
-                     "Stdev of M_3", dM3);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_3", stats->sampleLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_3", stats->sampleUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3",   PS_META_REPLACE, "M_3 = sqrt (M_c3^2 + M_s3^2)", vM3);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_ER", PS_META_REPLACE, "Stdev of M_3", dM3);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_LQ", PS_META_REPLACE, "Lower Quartile of M_3", stats->sampleLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_UQ", PS_META_REPLACE, "Upper Quartile of M_3", stats->sampleUQ);
 
     if (!psVectorStats(stats, M4, NULL, NULL, 0)) {
@@ -175,12 +206,8 @@
     float vM4 = stats->sampleMean;
     float dM4 = stats->sampleStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4",   PS_META_REPLACE,
-                     "M_4 = sqrt (M_c4^2 + M_s4^2)", vM4);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_ER", PS_META_REPLACE,
-                     "Stdev of M_4", dM4);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_4", stats->sampleLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_4", stats->sampleUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4",   PS_META_REPLACE, "M_4 = sqrt (M_c4^2 + M_s4^2)", vM4);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_ER", PS_META_REPLACE, "Stdev of M_4", dM4);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_LQ", PS_META_REPLACE, "Lower Quartile of M_4", stats->sampleLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_UQ", PS_META_REPLACE, "Upper Quartile of M_4", stats->sampleUQ);
 
 #else
@@ -192,8 +219,6 @@
     }
     float fwhm_major = stats->robustMedian;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW1",   PS_META_REPLACE,
-                     "FWHM of Major Axis from moments", stats->robustMedian);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW1_E", PS_META_REPLACE,
-                     "FWHM scatter (Major) from moments", stats->robustStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW1",   PS_META_REPLACE, "FWHM of Major Axis from moments", stats->robustMedian);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW1_E", PS_META_REPLACE, "FWHM scatter (Major) from moments", stats->robustStdev);
 
     if (!psVectorStats(stats, FWHM_MINOR, NULL, NULL, 0)) {
@@ -202,8 +227,6 @@
     }
     float fwhm_minor = stats->robustMedian;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW2",   PS_META_REPLACE,
-                     "FWHM of Minor Axis from moments", stats->robustMedian);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_FW2_E", PS_META_REPLACE,
-                     "FWHM scatter (Minor) from moments", stats->robustStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW2",   PS_META_REPLACE, "FWHM of Minor Axis from moments", stats->robustMedian);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_FW2_E", PS_META_REPLACE, "FWHM scatter (Minor) from moments", stats->robustStdev);
 
     if (!psVectorStats(stats, M2, NULL, NULL, 0)) {
@@ -213,12 +236,8 @@
     float vM2 = stats->robustMedian;
     float dM2 = stats->robustStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2",    PS_META_REPLACE,
-                     "M_2 = sqrt (M_c2^2 + M_s2^2)", vM2);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_ER", PS_META_REPLACE,
-                     "Stdev of M_2", dM2);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_2", stats->robustLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_2", stats->robustUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2",    PS_META_REPLACE, "M_2 = sqrt (M_c2^2 + M_s2^2)", vM2);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_ER", PS_META_REPLACE, "Stdev of M_2", dM2);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_LQ", PS_META_REPLACE, "Lower Quartile of M_2", stats->robustLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2_UQ", PS_META_REPLACE, "Upper Quartile of M_2", stats->robustUQ);
 
     if (!psVectorStats(stats, M2c, NULL, NULL, 0)) {
@@ -226,12 +245,8 @@
         goto FAIL;
     }
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C",   PS_META_REPLACE,
-                     "M_2c = sum f r^2 cos(2phi) / sum f", stats->robustMedian);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_E", PS_META_REPLACE,
-                     "Stdev of M_2c", stats->robustStdev);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_L", PS_META_REPLACE,
-                     "Lower Quartile of M_2c", stats->robustLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2C_U", PS_META_REPLACE,
-                     "Upper Quartile of M_2c", stats->robustUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C",   PS_META_REPLACE, "M_2c = sum f r^2 cos(2phi) / sum f", stats->robustMedian);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_E", PS_META_REPLACE, "Stdev of M_2c", stats->robustStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_L", PS_META_REPLACE, "Lower Quartile of M_2c", stats->robustLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2C_U", PS_META_REPLACE, "Upper Quartile of M_2c", stats->robustUQ);
 
     if (!psVectorStats(stats, M2s, NULL, NULL, 0)) {
@@ -239,12 +254,8 @@
         goto FAIL;
     }
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S",   PS_META_REPLACE,
-                     "M_2s = sum f r^2 cos(2phi) / sum f", stats->robustMedian);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_E", PS_META_REPLACE,
-                     "Stdev of M_2s", stats->robustStdev);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_L", PS_META_REPLACE,
-                     "Lower Quartile of M_2s", stats->robustLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M2S_U", PS_META_REPLACE,
-                     "Upper Quartile of M_2s", stats->robustUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S",   PS_META_REPLACE, "M_2s = sum f r^2 cos(2phi) / sum f", stats->robustMedian);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_E", PS_META_REPLACE, "Stdev of M_2s", stats->robustStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_L", PS_META_REPLACE, "Lower Quartile of M_2s", stats->robustLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M2S_U", PS_META_REPLACE, "Upper Quartile of M_2s", stats->robustUQ);
 
     if (!psVectorStats(stats, M3, NULL, NULL, 0)) {
@@ -254,12 +265,8 @@
     float vM3 = stats->robustMedian;
     float dM3 = stats->robustStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3",   PS_META_REPLACE,
-                     "M_3 = sqrt (M_c3^2 + M_s3^2)", vM3);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_ER", PS_META_REPLACE,
-                     "Stdev of M_3", dM3);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_3", stats->robustLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M3_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_3", stats->robustUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3",   PS_META_REPLACE, "M_3 = sqrt (M_c3^2 + M_s3^2)", vM3);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_ER", PS_META_REPLACE, "Stdev of M_3", dM3);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_LQ", PS_META_REPLACE, "Lower Quartile of M_3", stats->robustLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M3_UQ", PS_META_REPLACE, "Upper Quartile of M_3", stats->robustUQ);
 
     if (!psVectorStats(stats, M4, NULL, NULL, 0)) {
@@ -269,18 +276,14 @@
     float vM4 = stats->robustMedian;
     float dM4 = stats->robustStdev;
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4",   PS_META_REPLACE,
-                     "M_4 = sqrt (M_c4^2 + M_s4^2)", vM4);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_ER", PS_META_REPLACE,
-                     "Stdev of M_4", dM4);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_LQ", PS_META_REPLACE,
-                     "Lower Quartile of M_4", stats->robustLQ);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "IQ_M4_UQ", PS_META_REPLACE,
-                     "Upper Quartile of M_4", stats->robustUQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4",   PS_META_REPLACE, "M_4 = sqrt (M_c4^2 + M_s4^2)", vM4);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_ER", PS_META_REPLACE, "Stdev of M_4", dM4);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_LQ", PS_META_REPLACE, "Lower Quartile of M_4", stats->robustLQ);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "IQ_M4_UQ", PS_META_REPLACE, "Upper Quartile of M_4", stats->robustUQ);
 #endif
 
-    psLogMsg ("psphot", PS_LOG_INFO, "Image Quality Stats from %ld psf stars : FWHM (major, minor) : %f, %f\n",
+    psLogMsg ("psphot", PS_LOG_INFO, "Image Quality Stats from %ld psf stars : FWHM (major, minor) [pixels]: %f, %f\n",
               M2->n, fwhm_major, fwhm_minor);
 
-    psLogMsg ("psphot", PS_LOG_INFO, "M_2 : %f +/- %f, M_3 : %f +/- %f, M_4 : %f +/- %f\n",
+    psLogMsg ("psphot", PS_LOG_INFO, "M_2 : %f +/- %f, M_3 : %f +/- %f, M_4 : %f +/- %f  [pixels^n]\n",
               vM2, dM2, vM3, dM3, vM4, dM4);
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotKernelFromPSF.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotKernelFromPSF.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotKernelFromPSF.c	(revision 27840)
@@ -4,8 +4,8 @@
 
     assert (source);
-    assert (source->psfFlux); // XXX build if needed?
+    assert (source->psfImage); // XXX build if needed?
 
-    int x0 = source->peak->xf - source->psfFlux->col0;
-    int y0 = source->peak->yf - source->psfFlux->row0;
+    int x0 = source->peak->xf - source->psfImage->col0;
+    int y0 = source->peak->yf - source->psfImage->row0;
 
     // need to decide on the size: dynamically? statically?
@@ -17,12 +17,12 @@
     // if the realized PSF for this object does not cover the full kernel, give up for now
     if (x0 + psf->xMin < 0) goto escape;
-    if (x0 + psf->xMax >= source->psfFlux->numCols) goto escape;
+    if (x0 + psf->xMax >= source->psfImage->numCols) goto escape;
     if (y0 + psf->yMin < 0) goto escape;
-    if (y0 + psf->yMax >= source->psfFlux->numRows) goto escape;
+    if (y0 + psf->yMax >= source->psfImage->numRows) goto escape;
 
     double sum = 0.0;
     for (int j = psf->yMin; j <= psf->yMax; j++) {
 	for (int i = psf->xMin; i <= psf->xMax; i++) {
-	    double value = source->psfFlux->data.F32[y0 + j][x0 + i];
+	    double value = source->psfImage->data.F32[y0 + j][x0 + i];
 	    psf->kernel[j][i] = value;
 	    sum += value;
Index: branches/simtest_nebulous_branches/psphot/src/psphotLoadPSF.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotLoadPSF.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotLoadPSF.c	(revision 27840)
@@ -1,28 +1,79 @@
 # include "psphotInternal.h"
 
+// NOTE : pmPSF_IO.c functions must load the psf model onto the chip->analysis metadata because
+// the I/O operation likely occurs before the readout exists.  This implementation assumes that
+// a single psf model is valid for the entire set of readouts (not valid for a time series of readouts)
+
+// XXX for now (2010.01.27), the supporting programs do not define multiple PSPHOT.PSF.LOAD
+// files to go with multiple PSPHOT.INPUT files.  as a result, the implementation below is
+// currently going to work for the case of a single input file, but will fail if we try with a
+// stack of images.
+
 // load an externally supplied psf model
-pmPSF *psphotLoadPSF (pmConfig *config, const pmFPAview *view, psMetadata *recipe) {
+bool psphotLoadPSFReadout (pmConfig *config, const pmFPAview *view, const char *outFilename, const char *inFilename, int index) {
+
+    bool status;
+
+    // find the currently selected readout
+    pmFPAfile *inFile = pmFPAfileSelectSingle(config->files, inFilename, index); // File of interest
+    if (inFile == NULL) {
+        psLogMsg ("psphot", 3, "no psf supplied for this chip");
+        return true;
+    }
+
+    // find the currently selected readout
+    pmFPAfile *outFile = pmFPAfileSelectSingle(config->files, outFilename, index); // File of interest
+    if (outFile == NULL) {
+        psLogMsg ("psphot", 3, "no psf supplied for this chip");
+        return true;
+    }
 
     // find the currently selected chip
-    pmChip *chip = pmFPAfileThisChip (config->files, view, "PSPHOT.PSF.LOAD");
-    if (!chip) return NULL;
+    pmChip *chip = pmFPAviewThisChip (view, inFile->fpa);
+    if (!chip) return false;
 
     // find the currently selected readout
-    pmReadout *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.PSF.LOAD");
-    if (!readout) return NULL;
+    pmReadout *readout = pmFPAviewThisReadout (view, outFile->fpa);
+    if (!readout) return false;
 
     // check if a PSF model is supplied by the user
-    pmPSF *psf = psMetadataLookupPtr (NULL, chip->analysis, "PSPHOT.PSF");
+    pmPSF *psf = psMetadataLookupPtr (&status, chip->analysis, "PSPHOT.PSF");
     if (psf == NULL) {
         psLogMsg ("psphot", 3, "no psf supplied for this chip");
-        return NULL;
+        return true;
     }
 
-    if (!psphotPSFstats (readout, recipe, psf)) psAbort("cannot measure PSF shape terms");
+    if (!psphotPSFstats (readout, psf)) {
+        psAbort("cannot measure PSF shape terms");
+    }
 
     psLogMsg ("psphot", 3, "using externally supplied PSF model for this readout");
 
-    // we return a psf which can be free (and which may be removed from the metadata)
-    psMemIncrRefCounter (psf);
-    return psf;
+    // save PSF on readout->analysis
+    if (!psMetadataAddPtr (readout->analysis, PS_LIST_TAIL, "PSPHOT.PSF", PS_META_REPLACE | PS_DATA_UNKNOWN, "psphot psf model", psf)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "problem saving sources on readout");
+        return false;
+    }
+
+    return true;
 }
+
+bool psphotLoadPSF (pmConfig *config, const pmFPAview *view) {
+
+    bool status = false;
+
+    // XXX PSPHOT.PSF.LOAD vs PSPHOT.INPUT -- see note at top
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+
+        // Generate the mask and weight images, including the user-defined analysis region of interest
+        if (!psphotLoadPSFReadout (config, view, "PSPHOT.INPUT", "PSPHOT.PSF.LOAD", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to load PSF model for PSPHOT.PSF.LOAD entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotLoadSRCTEXT.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotLoadSRCTEXT.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotLoadSRCTEXT.c	(revision 27840)
@@ -0,0 +1,135 @@
+# include "psphotInternal.h"
+
+bool psphotLoadSRCTEXT (pmFPA *fpa, pmConfig *config) {
+
+    pmChip *chip;
+
+    // we have a psArray of filenames, but there is no information to tie the filenames to the
+    // corresponding chip/cell/readout.  Fortunately: (a) psphot only really operates on chips
+    // and (b) we can make some basic assumptions:
+
+    // 1) if there is only a single entry in the array, read the file and attach the results to
+    // each of the chip->analysis metadata entries (just an extra ref counter).
+
+    // 2) if there are multiple entries, the number of filenames must match the number of
+    // chips.
+
+    psArray *files = psMetadataLookupPtr(NULL, config->arguments, "SRCTEXT");
+
+    // XXX is this allowed?  how do we get an empty entry in arguments?
+    if (!files->n) return true;
+
+    // we use a default model type of GAUSS to define the sources
+    pmModelType modelType = pmModelClassGetType("PS_MODEL_GAUSS");
+
+    psArray *sourceArrays = psArrayAllocEmpty(files->n);
+    for (int i = 0; i < files->n; i++) {
+
+	char *filename = files->data[i];
+
+	// each file contains a list of X Y coordinates and nothing else?
+	FILE *f = fopen (filename, "r");
+	if (f == NULL) {
+            psError(PSPHOT_ERR_CONFIG, true, "Failed to read source text list %s", filename);
+	    return false;
+	}
+
+	double X, Y;
+
+	psArray *sources = psArrayAllocEmpty (100);
+	while (fscanf (f, "%lf %lf", &X, &Y) != EOF) {
+
+	    psEllipseAxes axes;
+	    
+	    pmSource *source = pmSourceAlloc ();
+	    pmModel *model = pmModelAlloc (modelType);
+	    source->modelPSF  = model;
+	    source->type = PM_SOURCE_TYPE_STAR;
+
+	    // NOTE: A SEGV here because "model" is NULL is probably caused by not initialising the models.
+	    psF32 *PAR = model->params->data.F32;
+	    psF32 *dPAR = model->dparams->data.F32;
+
+	    // NOTE: most of these values are irrelevant for loaded source positions
+	    source->seq       = 0;
+	    PAR[PM_PAR_XPOS]  = X;
+	    PAR[PM_PAR_YPOS]  = Y;
+	    dPAR[PM_PAR_XPOS] = 0.0;
+	    dPAR[PM_PAR_YPOS] = 0.0;
+	    axes.major        = 1.0;
+	    axes.minor        = 1.0;
+	    axes.theta        = 0.0;
+
+	    PAR[PM_PAR_SKY]   = 0.0;
+	    dPAR[PM_PAR_SKY]  = 0.0;
+	    source->sky       = PAR[PM_PAR_SKY];
+	    source->skyErr    = dPAR[PM_PAR_SKY];
+
+	    source->psfMag    = 0.0;
+	    source->errMag    = 0.0;
+	    source->apMag     = 0.0;
+
+	    PAR[PM_PAR_I0]    = 1.0;
+	    dPAR[PM_PAR_I0]   = 0.0;
+
+	    pmPSF_AxesToModel (PAR, axes);
+
+	    float peakFlux    = 1.0;
+
+	    source->peak = pmPeakAlloc(PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], peakFlux, PM_PEAK_LONE);
+	    source->peak->flux = peakFlux;
+	    source->peak->dx   = dPAR[PM_PAR_XPOS];
+	    source->peak->dy   = dPAR[PM_PAR_YPOS];
+	    source->peak->xf   = PAR[PM_PAR_XPOS]; // the alloc function takes the pixel index,
+	    source->peak->yf   = PAR[PM_PAR_YPOS]; // but we know the pixel coordinate
+
+	    source->pixWeight = 1.0;
+	    source->crNsigma  = 0.0;
+	    source->extNsigma = 0.0;
+	    source->apRadius  = 0.0;
+
+	    model->chisq      = 0.0;
+	    model->nDOF       = 0;
+	    model->nPix       = 0;
+
+	    source->moments = pmMomentsAlloc ();
+	    source->moments->Mxx = 0.0;
+	    source->moments->Mxy = 0.0;
+	    source->moments->Myy = 0.0;
+
+	    source->mode = 0;
+	    
+	    psArrayAdd (sources, 100, source);
+	    psFree(source);
+	}
+	psArrayAdd (sourceArrays, 100, sources);
+	psFree (sources);
+    }
+
+    if (files->n == 1) {
+	psArray *sources = sourceArrays->data[0];
+	pmFPAview *view = pmFPAviewAlloc (0);
+	while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+	    if (!chip->process) { continue; }
+	    psMetadataAddPtr (chip->analysis, PS_LIST_TAIL, "PSPHOT.SOURCES.TEXT", PS_DATA_ARRAY | PS_META_REPLACE, "loaded source from textfile", sources);
+	}
+	psFree (view);
+    } else {
+	int nSrc = 0;
+	pmFPAview *view = pmFPAviewAlloc (0);
+	while ((chip = pmFPAviewNextChip (view, fpa, 1)) != NULL) {
+	    if (!chip->process) { continue; }
+
+	    if (nSrc >= sourceArrays->n) {
+		psError(PSPHOT_ERR_CONFIG, true, "mismatch between number of source lists and number of chips (readouts)");
+		return false;
+	    }
+	    psArray *sources = sourceArrays->data[nSrc];
+	    psMetadataAddPtr (chip->analysis, PS_LIST_TAIL, "PSPHOT.SOURCES.TEXT", PS_DATA_ARRAY | PS_META_REPLACE, "loaded source from textfile", sources);
+	    nSrc ++;
+	}
+	psFree (view);
+    }
+    psFree (sourceArrays);
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotMagnitudes.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMagnitudes.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMagnitudes.c	(revision 27840)
@@ -1,14 +1,58 @@
 # include "psphotInternal.h"
 
-bool psphotMagnitudes(pmConfig *config, pmReadout *readout, const pmFPAview *view, psArray *sources, pmPSF *psf) {
+bool psphotMagnitudes (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+
+	// find the currently selected readout
+	pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", i); // File of interest
+	psAssert (file, "missing file?");
+
+	pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+	psAssert (readout, "missing readout?");
+
+	pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+	psAssert (detections, "missing detections?");
+
+	psArray *sources = detections->allSources;
+	psAssert (sources, "missing sources?");
+
+	pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+	psAssert (psf, "missing psf?");
+
+	if (!psphotMagnitudesReadout (config, recipe, view, readout, sources, psf)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to measure magnitudes for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotMagnitudesReadout(pmConfig *config, psMetadata *recipe, const pmFPAview *view, pmReadout *readout, psArray *sources, pmPSF *psf) {
 
     bool status = false;
     int Nap = 0;
 
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping source magnitudes");
+	return true;
+    }
+	
     psTimerStart ("psphot.mags");
-
-    // select the appropriate recipe information
-    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
-    assert (recipe);
 
     // determine the number of allowed threads
@@ -41,10 +85,12 @@
     }
 
-    bool IGNORE_GROWTH = psMetadataLookupBool (&status, recipe, "IGNORE_GROWTH");
+    bool IGNORE_GROWTH  = psMetadataLookupBool (&status, recipe, "IGNORE_GROWTH");
     bool INTERPOLATE_AP = psMetadataLookupBool (&status, recipe, "INTERPOLATE_AP");
+    bool DIFF_STATS     = psMetadataLookupBool (&status, recipe, "INTERPOLATE_AP");
 
     pmSourcePhotometryMode photMode = PM_SOURCE_PHOT_APCORR | PM_SOURCE_PHOT_WEIGHT;
     if (!IGNORE_GROWTH) photMode |= PM_SOURCE_PHOT_GROWTH;
     if (INTERPOLATE_AP) photMode |= PM_SOURCE_PHOT_INTERP;
+    if (DIFF_STATS)     photMode |= PM_SOURCE_PHOT_DIFFSTATS;
 
     // choose Cx, Cy (see psphotThreadTools.c for overview of the concepts)
@@ -71,4 +117,5 @@
             PS_ARRAY_ADD_SCALAR(job->args, photMode, PS_TYPE_S32);
             PS_ARRAY_ADD_SCALAR(job->args, maskVal,  PS_TYPE_IMAGE_MASK);
+            PS_ARRAY_ADD_SCALAR(job->args, markVal,  PS_TYPE_IMAGE_MASK);
             PS_ARRAY_ADD_SCALAR(job->args, 0,        PS_TYPE_S32); // this is used as a return value for nAp
 
@@ -102,5 +149,5 @@
                 fprintf (stderr, "error with job\n");
             } else {
-                psScalar *scalar = job->args->data[7];
+                psScalar *scalar = job->args->data[8];
                 Nap += scalar->data.S32;
             }
@@ -127,9 +174,26 @@
     pmSourcePhotometryMode photMode = PS_SCALAR_VALUE(job->args->data[5],S32);
     psImageMaskType maskVal         = PS_SCALAR_VALUE(job->args->data[6],PS_TYPE_IMAGE_MASK_DATA);
+    psImageMaskType markVal         = PS_SCALAR_VALUE(job->args->data[7],PS_TYPE_IMAGE_MASK_DATA);
 
     for (int i = 0; i < sources->n; i++) {
         pmSource *source = (pmSource *) sources->data[i];
-        status = pmSourceMagnitudes (source, psf, photMode, maskVal);
+
+        // replace object in image
+        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+        }
+
+	// clear the mask bit and set the circular mask pixels
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal));
+	psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, source->apRadius, "OR", markVal);
+
+        status = pmSourceMagnitudes (source, psf, photMode, maskVal); // maskVal includes markVal
         if (status && isfinite(source->apMag)) Nap ++;
+
+	// clear the mask bit 
+	psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal));
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
 
         if (backModel) {
@@ -155,48 +219,9 @@
 
     // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    psScalar *scalar = job->args->data[7];
+    psScalar *scalar = job->args->data[8];
     scalar->data.S32 = Nap;
 
     return true;
 }
-
-# if (0)
-bool psphotMagnitudes_Unthreaded (int *nap, psArray *sources, pmPSF *psf, psImageBinning *binning, pmReadout *backModel, pmReadout *backStdev, pmSourcePhotometryMode photMode, psImageMaskType maskVal) {
-
-    bool status;
-    int Nap = 0;
-
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = (pmSource *) sources->data[i];
-        status = pmSourceMagnitudes (source, psf, photMode, maskVal);
-        if (status && isfinite(source->apMag)) Nap ++;
-
-        if (backModel) {
-            psAssert (binning, "if backModel is defined, so should binning be");
-            source->sky = psImageUnbinPixel(source->peak->x, source->peak->y, backModel->image, binning);
-            if (isnan(source->sky) && false) {
-                psLogMsg ("psphot.magnitudes", PS_LOG_DETAIL, "error setting pmSource.sky");
-            }
-        } else {
-            source->sky = NAN;
-        }
-
-        if (backStdev) {
-            psAssert (binning, "if backStdev is defined, so should binning be");
-            source->skyErr = psImageUnbinPixel(source->peak->x, source->peak->y, backStdev->image, binning);
-            if (isnan(source->skyErr) && false) {
-                psLogMsg ("psphot.magnitudes", PS_LOG_DETAIL, "error setting pmSource.skyErr");
-            }
-        } else {
-            source->skyErr = NAN;
-        }
-    }
-
-    // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    *nap = Nap;
-
-    return true;
-}
-# endif
 
 bool psphotPSFWeights(pmConfig *config, pmReadout *readout, const pmFPAview *view, psArray *sources) {
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakeFluxScale.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakeFluxScale.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakeFluxScale.c	(revision 27840)
@@ -60,4 +60,9 @@
         goto DONE;
     }
+    if (trend->mode == PM_TREND_MAP) {
+	// p_psImagePrint (2, trend->map->map, "FluxScale Before"); // XXX TEST:
+	psImageMapRepair (trend->map->map);
+	// p_psImagePrint (2, trend->map->map, "FluxScale After"); // XXX TEST:
+    }
 
     // XXX do something useful to measure residual statistics
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakePSF.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakePSF.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakePSF.c	(revision 27840)
@@ -0,0 +1,35 @@
+# include "psphotStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+    pmErrorRegister();                  // register psModule's error codes/messages
+    psphotInit();
+
+    // load command-line arguments, options, and system config data
+    pmConfig *config = psphotMakePSFArguments (argc, argv);
+    assert(config);
+
+    psphotVersionPrint();
+
+    // load input data (config and images (signal, noise, mask)
+    if (!psphotParseCamera (config)) {
+        psErrorStackPrint(stderr, "Error setting up the camera\n");
+        exit (psphotGetExitStatus());
+    }
+
+    // call psphot for each readout
+    if (!psphotMakePSFImageLoop (config)) {
+        psErrorStackPrint(stderr, "Error in the psphot image loop\n");
+        exit (psphotGetExitStatus());
+    }
+
+    psLogMsg ("psphot", 3, "complete psphot run: %f sec\n", psTimerMark ("complete"));
+
+    psErrorCode exit_status = psphotGetExitStatus();
+    psphotCleanup (config);
+    exit (exit_status);
+}
+
+// all functions which return to this level must raise one of the top-level error codes if they
+// exit with an error.  these error codes are used to specify the program exit status
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakePSFArguments.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakePSFArguments.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakePSFArguments.c	(revision 27840)
@@ -0,0 +1,189 @@
+# include "psphotStandAlone.h"
+
+static void writeHelpInfo(const char* program, pmConfig* config, FILE* ofile)
+{
+  fprintf(ofile,
+	  "Usage: one of the following\n"
+	  "%s -file fname1[,fname2,...] -mask maskfile1[,maskfile2,...]\n"
+	  "     -variance varfile1[,varfile2,...] OutFileBaseName\n"
+	  "\n"
+	  "%s -list FileNameList [-masklist MaskFileNameList] \n"
+	  "     -variancelist VarFileNameList OutFileBaseName\n"
+	  "\n"
+	  "%s -help\n"
+	  "\n"
+	  "%s -version\n"
+	  "\n"
+	  "where:\n"
+	  "  FileNameList is a text file containing filenames, one per line\n"
+	  "  MaskFileNameList is a text file of mask filenames, one per line\n"
+	  "  VarFileNameList is a text file of variance filenames, one per line\n"
+	  "  OutFileBaseName is the 'root name' for output files\n"
+	  "\n"
+	  "additional options:\n"
+	  "  -psf PsfFile1[,PsfFile2,...] or -psflist PsfFileNameList\n"
+	  "     specify PSF rather than letting %s estimate it\n"
+	  "  -src SrcFile1[,SrcFile2,...] or -srclist SrcFileNameList\n"
+	  "     specify additional sources for PSF generation\n"
+	  "  -chip nn[,nn,...]\n"
+	  "     select detector chips to process; default is all.\n"
+	  "     Indices correspond to zero-based offset in the FPA metadata table.\n"
+	  "  -photcode PhotoCodeName\n"
+	  "     specify photocode\n"
+	  "  -region RegionString\n"
+	  "     specify analysis region.  String is of form '[x0:x1,y0:y1]'\n"
+	  "     To use this option you must define a default in psphot.config\n"
+	  "  -visual\n"
+	  "     turns on interactive display mode\n"
+	  "  -dumpconfig CfgFileName\n"
+          "     causes config info to be dumped to the named file.\n"
+	  "  -break NOTHING|BACKMDL|PEAKS|MOMENTS|PSFMODEL|ENSEMBLE|PASS1\n"
+	  "     choose a point at which to exit processing early\n"
+	  "  -nthreads n\n"
+	  "     set number of parallel threads of execution\n"
+	  "  -F OldFileRule ReplacementFileRule\n"
+	  "     change file naming rule; e.g. '-F PSPHOT.OUTPUT PSPHOT.OUT.CMF.MEF'\n"
+	  "  -D name stringval\n"
+	  "     set a string-valued config parameter\n"
+	  "  -Di name intval\n"
+	  "     set an integer-valued config parameter\n"
+	  "  -Df name fval\n"
+	  "     set a float-valued config parameter\n"
+	  "  -Db name boolval\n"
+	  "     set a boolean-valued config parameter\n"
+	  "  -v, -vv, -vvv\n"
+	  "     set increasing levels of verbosity\n"
+	  "  -logfmt FormatString\n"
+	  "     set format string used for log messages\n"
+	  "  -trace Fac Lvl\n"
+	  "     set tracing for facility Fac to integer Lvl, e.g. '-trace err 10'\n"
+	  "  -trace-levels\n"
+	  "     print current trace levels\n",
+	  program,program,program,program,program);
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+    exit(PS_EXIT_SUCCESS);
+}
+
+static void usage(const char *program,  // Name of the program
+                  psMetadata *arguments, // Command-line arguments
+                  pmConfig *config,      // Configuration
+		  int exitCode
+		  ) 
+{
+  fprintf(stderr,
+	  "Usage: one of the following\n"
+	  "%s -file fname1[,fname2,...] -mask maskfile1[,maskfile2,...]\n"
+	  "     -variance varfile1[,varfile2,...] OutFileBaseName\n"
+	  "\n"
+	  "%s -list FileNameList [-masklist MaskFileNameList] \n"
+	  "     -variancelist VarFileNameList OutFileBaseName\n"
+	  "\n"
+	  "Try '%s -help' for more options and explanation\n",
+	  program,program,program);
+    if (exitCode != PS_EXIT_SUCCESS)
+      psErrorStackPrint(stderr, "Error reading arguments\n");
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+    exit(exitCode);
+}
+
+pmConfig *psphotMakePSFArguments(int argc, char **argv) {
+
+    int N;
+    bool status;
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSPHOT_RECIPE);
+    if (config == NULL) {
+      psErrorStackPrint(stderr, "Can't read site configuration");
+	exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    PSARGUMENTS_INSTANTIATE_GENERICS( psphot, config, argc, argv );
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSPHOT recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSPHOT_RECIPE);
+
+    // Number of threads is handled
+    PSARGUMENTS_INSTANTIATE_THREADSARG( psphot, config, argc, argv )
+
+    // visual : interactive display mode
+    if ((N = psArgumentGet (argc, argv, "-visual"))) {
+        psArgumentRemove (N, &argc, argv);
+        pmVisualSetVisual(true);
+    }
+
+    // break : used from recipe throughout psphotReadout
+    if ((N = psArgumentGet (argc, argv, "-break"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "BREAK_POINT", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // analysis region : overrides recipe value, used in psphotReadout/psphotEnsemblePSF
+    if ((N = psArgumentGet (argc, argv, "-region"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "ANALYSIS_REGION", 0, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // chip selection is used to limit chips to be processed
+    if ((N = psArgumentGet (argc, argv, "-chip"))) {
+	if (argc<=N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (config->arguments, PS_LIST_TAIL, "CHIP_SELECTIONS", PS_DATA_STRING, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // if these command-line options are supplied, load the file name lists into config->arguments
+    // override any configuration-specified source for these files
+    //
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "MASK",       "-mask",     "-masklist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "VARIANCE",   "-variance", "-variancelist");
+
+    // an input list of sources to use is allowed, but not required
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "SRC", "-src", "-srclist");
+    pmConfigFileSetsMD (config->arguments, &argc, argv, "SRCTEXT", "-srctext", "-srctextlist");
+
+    if (argc == 1) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Too few arguments: %d", argc);
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    if (psArgumentGet(argc, argv, "-help") ||
+	psArgumentGet(argc, argv, "-h"))
+      writeHelpInfo(argv[0], config, stdout);
+      
+    // the input file is a required argument; if not found, we will exit
+    status = pmConfigFileSetsMD (config->arguments, &argc, argv, "INPUT", "-file", "-list");
+    if (!status) {
+        psError(PSPHOT_ERR_ARGUMENTS, false, "pmConfigFileSetsMD failed to parse arguments");
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    if (argc != 2) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Expected to see one more argument; saw %d", argc - 1);
+	usage(argv[0], config->arguments, config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    // output position is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psphot", 1, "Done with psphotMakePSFArguments...\n");
+    return (config);
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakePSFImageLoop.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakePSFImageLoop.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakePSFImageLoop.c	(revision 27840)
@@ -0,0 +1,129 @@
+# include "psphotStandAlone.h"
+
+# define ESCAPE(MESSAGE) { \
+  psError(PSPHOT_ERR_DATA, false, MESSAGE); \
+  psFree (view); \
+  return false; \
+}
+
+bool psphotMakePSFImageLoop (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    pmFPAfile *load = psMetadataLookupPtr (&status, config->files, "PSPHOT.LOAD");
+    if (!status) {
+        psError(PSPHOT_ERR_PROG, false, "Can't find input data!");
+        return false;
+    }
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSPHOT.INPUT");
+    if (!status) {
+        psError(PSPHOT_ERR_PROG, false, "Can't find input data!");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+    pmHDU *lastHDU = NULL;              // Last HDU updated
+
+    // files associated with the science image
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for fpa in psphot.");
+
+    // for psphot, we force data to be read at the chip level
+    while ((chip = pmFPAviewNextChip (view, load->fpa, 1)) != NULL) {
+        psLogMsg ("psphot", 4, "Chip %d: %x %x\n", view->chip, chip->file_exists, chip->process);
+        if (! chip->process || ! chip->file_exists) { continue; }
+
+        // load just the input image data (image, mask, weight)
+        pmFPAfileActivate (config->files, false, NULL);
+        pmFPAfileActivate (config->files, true, "PSPHOT.LOAD");
+        pmFPAfileActivate (config->files, true, "PSPHOT.MASK");
+        pmFPAfileActivate (config->files, true, "PSPHOT.VARIANCE");
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for Chip in psphot.");
+
+        // mosaic the cells of a chip into a single contiguous (trimmed) chip
+        if (!psphotMosaicChip(config, view, "PSPHOT.INPUT", "PSPHOT.LOAD")) ESCAPE ("Unable to mosaic chip.");
+
+        // try to load other supporting data (PSF, SRC, etc).
+        // do not re-load the following three files
+        pmFPAfileActivate (config->files, true, NULL);
+        pmFPAfileActivate (config->files, false, "PSPHOT.LOAD");
+        pmFPAfileActivate (config->files, false, "PSPHOT.MASK");
+        pmFPAfileActivate (config->files, false, "PSPHOT.VARIANCE");
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for Chip in psphot.");
+
+        // re-activate files so they will be closed and freed below
+        pmFPAfileActivate (config->files, true, NULL);
+
+        // there is now only a single chip (multiple readouts?). loop over it and process
+        while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psLogMsg ("psphot", 5, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+                psLogMsg ("psphot", 6, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+                if (! readout->data_exists) { continue; }
+
+                // Update the header
+		pmHDU *hdu = pmHDUGetHighest(input->fpa, chip, cell);
+		if (hdu && hdu != lastHDU) {
+		    psphotVersionHeaderFull(hdu->header);
+		    lastHDU = hdu;
+                }
+
+		// if an external mask is supplied, ensure that NAN pixels are also masked
+		if (readout->mask) {
+		    psImageMaskType maskSat = pmConfigMaskGet("SAT", config); // Mask value for saturated pixels
+		    if (!pmReadoutMaskNonfinite(readout, maskSat)) {
+			psError(psErrorCodeLast(), false, "Unable to mask non-finite pixels.");
+			psFree(view);
+			return false;
+		    }
+		}
+
+                // run the actual photometry analysis on this chip/cell/readout
+                if (!psphotMakePSFReadout (config, view)) {
+                    psError(psErrorCodeLast(), false, "failure in psphotReadout for chip %d, cell %d, readout %d\n", view->chip, view->cell, view->readout);
+                    psFree (view);
+                    return false;
+                }
+            }
+
+            status = true;
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL");
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL.STDEV");
+            status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKGND");
+            if (!status) {
+                psError(PSPHOT_ERR_PROG, false, "trouble dropping internal files");
+                psFree (view);
+                return false;
+            }
+        }
+
+        // save output which is saved at the chip level
+        if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed output for Chip in psphot.");
+    }
+    // save output which is saved at the fpa level
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed ouput for FPA in psphot.");
+
+    // fail if we failed to handle an error
+    if (psErrorCodeLast() != PS_ERR_NONE) psAbort ("failed to handle an error!");
+
+    psFree (view);
+    return true;
+}
+
+// I/O files related to psphot:
+// PSPHOT.INPUT   : input image file(s)
+// PSPHOT.RESID   : residual image
+// PSPHOT.OUTPUT  : output object tables (object)
+
+// PSPHOT.BACKSUB : background subtracted image
+// PSPHOT.BACKGND : background model (full-scale image?)
+// PSPHOT.BACKMDL : background model (binned image?)
+// PSPHOT.PSF     : sample PSF images
+
+// PSPHOT.MASK
+// PSPHOT.VARIANCE
+//
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakePSFReadout.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakePSFReadout.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakePSFReadout.c	(revision 27840)
@@ -0,0 +1,76 @@
+# include "psphotInternal.h"
+
+bool psphotMakePSFReadout(pmConfig *config, const pmFPAview *view) {
+
+    // measure the total elapsed time in psphotReadout.  XXX the current threading plan
+    // for psphot envisions threading within psphotReadout, not multiple threads calling
+    // the same psphotReadout.  In the current plan, this dtime is the elapsed time used
+    // jointly by the multiple threads, not the total time used by all threads.
+    psTimerStart ("psphotReadout");
+
+    pmModelClassSetLimits(PM_MODEL_LIMITS_LAX);
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
+    if (!recipe) {
+        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
+        return false;
+    }
+
+    // set the photcode for this image
+    if (!psphotAddPhotcode (config, view)) {
+        psError (PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
+        return false;
+    }
+
+    // optional break-point for processing
+    char *breakPt = psMetadataLookupStr (NULL, recipe, "BREAK_POINT");
+    PS_ASSERT_PTR_NON_NULL (breakPt, false);
+
+    // Generate the mask and weight images, including the user-defined analysis region of interest
+    psphotSetMaskAndVariance (config, view);
+    if (!strcasecmp (breakPt, "NOTHING")) {
+        return psphotReadoutCleanup(config, view);
+    }
+
+    // generate a background model (median, smoothed image)
+    if (!psphotModelBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!psphotSubtractBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "BACKMDL")) {
+        return psphotReadoutCleanup (config, view);
+    }
+
+    psphotLoadExtSources (config, view);
+
+    // If sources have been supplied, then these should be used to measure the PSF include
+    // externally-supplied sources; if not, we need to generate a set of possible PSF sources.
+    // This function updates the SN entries for the loaded sources or generates a set of
+    // detections from the image, if no external ones have been supplied.  Sources loaded from
+    // a text file have no valid SN values, but psphotChoosePSF needs to select the top
+    // PSF_MAX_NSTARS to generate the PSF.
+    if (!psphotCheckExtSources (config, view)) {
+	psLogMsg ("psphot", 3, "failure to select possible PSF sources (external or internal)");
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // Use bright stellar objects to measure PSF. If we do not have enough stars to generate
+    // the PSF, build one from the SEEING guess and model class
+    if (!psphotChoosePSF (config, view)) {
+	psLogMsg ("psphot", 3, "failure to construct a psf model");
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // measure aperture photometry corrections
+# if 0
+    if (!psphotApResid (config, view)) {
+        psLogMsg ("psphot", 3, "failed on psphotApResid");
+        return psphotReadoutCleanup (config, view);
+    }
+# endif
+
+    return psphotReadoutCleanup (config, view);
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotMakeResiduals.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMakeResiduals.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMakeResiduals.c	(revision 27840)
@@ -1,3 +1,5 @@
 # include "psphotInternal.h"
+
+# define RESIDUAL_SOFTENING 0.005 
 
 bool psphotMakeResiduals (psArray *sources, psMetadata *recipe, pmPSF *psf, psImageMaskType maskVal) {
@@ -31,4 +33,7 @@
 
     float pixelSN = psMetadataLookupF32(&status, recipe, "PSF.RESIDUALS.PIX.SN");
+    PS_ASSERT (status, false);
+
+    float radiusMax = psMetadataLookupF32(&status, recipe, "PSF.RESIDUALS.RADIUS");
     PS_ASSERT (status, false);
 
@@ -171,5 +176,4 @@
                 bool offImage = false;
                 if (psImageInterpolate (&flux, &dflux, &mflux, ix, iy, interp) == PS_INTERPOLATE_STATUS_OFF) {
-                    // fprintf (stderr, "off image: %f %f : %f %f\n", ix, iy, flux, dflux);
                     // This pixel is off the image
                     offImage = true;
@@ -179,6 +183,9 @@
                 }
                 fluxes->data.F32[i] = flux;
-                dfluxes->data.F32[i] = dflux;
+                dfluxes->data.F32[i] = hypot(dflux, RESIDUAL_SOFTENING);
                 if (isnan(flux)) {
+                    fmasks->data.PS_TYPE_VECTOR_MASK_DATA[i] = badMask;
+                }
+                if (isnan(dflux)) {
                     fmasks->data.PS_TYPE_VECTOR_MASK_DATA[i] = badMask;
                 }
@@ -234,4 +241,10 @@
 		}
 
+		float radius = hypot((ox - 0.5*resid->Ro->numCols), (oy - 0.5*resid->Ro->numRows));
+		if (radius > radiusMax) {
+                  resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox] = 1;
+		  continue;
+                }
+
                 resid->Ro->data.F32[oy][ox] = psStatsGetValue(fluxStats, statOption);
                 resid->Rx->data.F32[oy][ox] = resid->Ry->data.F32[oy][ox] = 0.0;
@@ -248,9 +261,13 @@
                   resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox] = 1;
                 }
-
-                // fprintf (stderr, "res: %2d %2d : %6.4f  %6.4f  %6.4f   %3d  %1d\n", ox, oy, resid->Ro->data.F32[oy][ox], fluxStats->sampleStdev, fluxStats->sampleStdev/sqrt(nKeep), nKeep, resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox]);
-
             } else {
                 assert (SPATIAL_ORDER == 1);
+
+		float radius = hypot((ox - 0.5*resid->Ro->numCols), (oy - 0.5*resid->Ro->numRows));
+		if (radius > radiusMax) {
+                  resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox] = 1;
+		  continue;
+                }
+
                 psImageInit(A, 0.0);
                 psVectorInit(B, 0.0);
@@ -275,8 +292,7 @@
 
                 if (!psMatrixGJSolve(A, B)) {
-                    psError(PSPHOT_ERR_PSF, false, "Singular matrix solving for (y,x) = (%d,%d)'s residuals",
-                            oy, ox);
-                    psFree(resid); resid = NULL;
-                    break;
+		    resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox] = 1;
+                    psWarning("Singular matrix solving for (y,x) = (%d,%d)'s residuals, masking", oy, ox);
+		    continue;
                 }
 
@@ -286,11 +302,11 @@
 
                 float dRo = sqrt(A->data.F32[0][0]);
-                // fprintf (stderr, "res: %2d %2d : %6.4f  %6.4f  %6.4f   %3d  %1d\n",
-                // ox, oy, resid->Ro->data.F32[oy][ox], dRo, dRo/sqrt(nKeep), nKeep, resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox]);
 
                 if (fabs(resid->Ro->data.F32[oy][ox]) < pixelSN*dRo/sqrt(nKeep)) {
                   resid->mask->data.PM_TYPE_RESID_MASK_DATA[oy][ox] = 1;
-                }
-                //resid->variance->data.F32[oy][ox] = XXX;
+		  resid->Ro->data.F32[oy][ox] = 0.0;
+		  resid->Rx->data.F32[oy][ox] = 0.0;
+		  resid->Ry->data.F32[oy][ox] = 0.0;
+                }
             }
         }
Index: branches/simtest_nebulous_branches/psphot/src/psphotMaskReadout.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMaskReadout.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMaskReadout.c	(revision 27840)
@@ -1,12 +1,40 @@
 # include "psphotInternal.h"
 
+bool psphotSetMaskAndVariance (pmConfig *config, const pmFPAview *view) {
+
+    bool status = false;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+
+	// Generate the mask and weight images, including the user-defined analysis region of interest
+	if (!psphotSetMaskAndVarianceReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to generate mask for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
 // generate mask and variance if not defined, additional mask for restricted subregion
-bool psphotSetMaskAndVariance (pmConfig *config, pmReadout *readout, psMetadata *recipe) {
+bool psphotSetMaskAndVarianceReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
 
-    // ** Interpret the mask values:
-    // XXX drop the write to recipe and move config into psphotRoughClass?
-    // XXX alternatively, define a function to set the psphot recipe masks
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", index); // File of interest
+    psAssert (file, "missing file?");
+
+    // find the currently selected readout
+    pmReadout  *readout = pmFPAviewThisReadout (view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    // save maskSat and maskBad on the psphot recipe (mostly for psphotRoughClass)
     psImageMaskType maskSat  = pmConfigMaskGet("SAT", config); // Mask value for saturated pixels
     psMetadataAddImageMask (recipe, PS_LIST_TAIL, "MASK.SAT", PS_META_REPLACE, "user-defined mask", maskSat);
@@ -14,5 +42,5 @@
     psImageMaskType maskBad  = pmConfigMaskGet("LOW", config); // Mask value for low pixels
     if (!maskBad) {
-        // XXX: for backward compatability look up old name
+        // for backward compatability look up old name
         maskBad  = pmConfigMaskGet("BAD", config);
     }
@@ -33,13 +61,17 @@
     }
 
+    bool softenVariance = psMetadataLookupBool (&status, recipe, "SOFTEN.VARIANCE");
+    float softenFraction = psMetadataLookupF32 (&status, recipe, "SOFTEN.VARIANCE.FRACTION");
+
     // make this an option via the recipe
-    if (0) {
+    if (softenVariance) {
       psImage *im = readout->image;
       psImage *wt = readout->variance;
-      psImage *mk = readout->mask;
       for (int j = 0; j < im->numRows; j++) {
         for (int i = 0; i < im->numCols; i++) {
-          if (isfinite(im->data.F32[j][i]) && isfinite(wt->data.F32[j][i])) continue;
-          mk->data.PS_TYPE_IMAGE_MASK_DATA[j][i] |= maskBad;
+	    if (!isfinite(im->data.F32[j][i])) continue;
+	    if (!isfinite(wt->data.F32[j][i])) continue;
+	    float sysError = softenFraction * im->data.F32[j][i];
+	    wt->data.F32[j][i] += PS_SQR(sysError);
         }
       }
@@ -69,4 +101,7 @@
     }
 
+    // display the image, weight, mask (ch 1,2,3)
+    psphotVisualShowImage (readout);
+
     return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotMergeSources.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMergeSources.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMergeSources.c	(revision 27840)
@@ -5,57 +5,170 @@
                          PM_SOURCE_MODE_CR_LIMIT | PM_SOURCE_MODE_EXT_LIMIT) // Mask to apply for PSF sources
 
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotMergeSources (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (!psphotMergeSourcesReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to merge sources for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
+
 // add newly selected sources to the existing list of sources
-bool psphotMergeSources (psArray *oldSources, psArray *newSources) {
+bool psphotMergeSourcesReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index) {
+
+    bool status;
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *newSources = detections->newSources;
+    psAssert (newSources, "missing sources?");
+
+    // XXX TEST:
+    if (detections->allSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->allSources);
+    }
+    if (detections->newSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->newSources);
+    }
+
+    if (!detections->allSources) {
+        detections->allSources = psArrayAllocEmpty(newSources->n);
+    }
+    psArray *allSources = detections->allSources;
 
     for (int i = 0; i < newSources->n; i++) {
         pmSource *source = newSources->data[i];
-        psArrayAdd (oldSources, 100, source);
-    }
-    return true;
-}
-
-// merge the externally supplied sources with the existing sources.  mark them as having
-// mode PM_SOURCE_MODE_EXTERNAL
-bool psphotLoadExtSources (pmConfig *config, const pmFPAview *view, psArray *sources) {
-
-    // find the currently selected readout
-    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT.CMF");
-    if (!readout) {
-        psLogMsg ("psphot", 3, "no external sources supplied");
-        return true;
-    }
-
-    psArray *extSources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
-    if (!extSources) {
+        psArrayAdd (allSources, 100, source);
+    }
+
+    psFree (detections->newSources);
+    detections->newSources = NULL;
+
+    return true;
+}
+
+// Merge the externally supplied sources with the existing sources.  Mark them as having mode
+// PM_SOURCE_MODE_EXTERNAL.
+
+// XXX This function needs to be updated to loop over set of input files.  At the moment, we
+// only expect a single entry for PSPHOT.INPUT.CMF and PSPHOT.SOURCES.TEXT, so we can only
+// associate input sources with a single entry for PSPHOT.INPUT
+bool psphotLoadExtSources (pmConfig *config, const pmFPAview *view) {
+
+    bool status;
+    pmDetections *extCMF = NULL;
+    psArray *extSourcesTXT = NULL;
+    int index = 0;
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (!detections) {
+	detections = pmDetectionsAlloc();
+	detections->newSources = psArrayAllocEmpty (100);
+	// save detections on the readout->analysis
+	if (!psMetadataAddPtr (readout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_META_REPLACE | PS_DATA_UNKNOWN, "psphot detections", detections)) {
+	    psError (PSPHOT_ERR_CONFIG, false, "problem saving detections on readout");
+	    return false;
+	}
+    } else {
+	psMemIncrRefCounter(detections); // so we can free the detections below
+    }
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    // load data from input CMF file:
+    {
+        pmReadout *readoutCMF = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT.CMF");
+        if (!readoutCMF) goto loadTXT;
+
+        extCMF = psMetadataLookupPtr (NULL, readoutCMF->analysis, "PSPHOT.DETECTIONS");
+        if (extCMF) {
+            for (int i = 0; i < extCMF->allSources->n; i++) {
+                pmSource *source = extCMF->allSources->data[i];
+                source->mode |= PM_SOURCE_MODE_EXTERNAL;
+
+                // the supplied peak flux needs to be re-normalized
+                source->peak->flux = 1.0;
+                source->peak->value = 1.0;
+
+                // drop the loaded source modelPSF
+                psFree (source->modelPSF);
+                source->modelPSF = NULL;
+		source->imageID = index;
+
+                psArrayAdd (detections->newSources, 100, source);
+            }
+        }
+    }
+
+loadTXT:
+
+    // load data from input TXT file:
+    {
+        pmChip *chipTXT = pmFPAfileThisChip (config->files, view, "PSPHOT.INPUT");
+        if (!chipTXT) goto finish;
+
+        extSourcesTXT = psMetadataLookupPtr (NULL, chipTXT->analysis, "PSPHOT.SOURCES.TEXT");
+        if (extSourcesTXT) {
+            for (int i = 0; i < extSourcesTXT->n; i++) {
+                pmSource *source = extSourcesTXT->data[i];
+                source->mode |= PM_SOURCE_MODE_EXTERNAL;
+
+                // drop the loaded source modelPSF
+                psFree (source->modelPSF);
+                source->modelPSF = NULL;
+		source->imageID = index;
+
+                psArrayAdd (detections->newSources, 100, source);
+            }
+        }
+    }
+
+finish:
+
+    psFree (detections);
+
+    if (!extCMF && !extSourcesTXT) {
         psLogMsg ("psphot", 3, "no external sources for this readout");
         return true;
     }
 
-    for (int i = 0; i < extSources->n; i++) {
-        pmSource *source = extSources->data[i];
-        source->mode |= PM_SOURCE_MODE_EXTERNAL;
-        pmModel *model = source->modelPSF;
-
-        float xpos = model->params->data.F32[PM_PAR_XPOS];
-        float ypos = model->params->data.F32[PM_PAR_YPOS];
-
-        source->peak = pmPeakAlloc(xpos, ypos, 1.0, PM_PEAK_LONE);
-        source->peak->xf = xpos;
-        source->peak->yf = ypos;
-        source->peak->flux = 1.0;
-
-        // drop the loaded source modelPSF
-        psFree (source->modelPSF);
-        source->modelPSF = NULL;
-    }
-
-    psphotMergeSources (sources, extSources);
-    psLogMsg ("psphot", 3, "%ld external sources merged to yield %ld total sources", extSources->n, sources->n);
-
+    int nCMF = extCMF        ? extCMF->allSources->n        : 0;
+    int nTXT = extSourcesTXT ? extSourcesTXT->n : 0;
+
+    psLogMsg ("psphot", 3, "%d external sources (%d cmf, %d text) merged to yield %ld total sources",
+              nCMF + nTXT, nCMF, nTXT, sources->n);
     return true;
 }
 
 // extract the input sources corresponding to this readout
+// XXX this function needs to be updated to work with the new context of pshot inputs
 psArray *psphotLoadPSFSources (pmConfig *config, const pmFPAview *view) {
+
+    bool status;
 
     // find the currently selected readout
@@ -66,7 +179,14 @@
     }
 
-    psArray *sources = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.SOURCES");
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    if (!detections) {
+        psLogMsg ("psphot", 3, "no psf sources for this readout");
+        return NULL;
+    }
+
+    psArray *sources = detections->allSources;
     if (!sources) {
         psLogMsg ("psphot", 3, "no psf sources for this readout");
+        return NULL;
     }
 
@@ -74,6 +194,45 @@
 }
 
+// this function is used to fix sources which were loaded externally, but have passed from
+// psphotDetectionsFromSources to psphotSourceStats and are now stored on
+// detections->newSources.
+bool psphotRepairLoadedSources (pmConfig *config, const pmFPAview *view) {
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", 0); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (NULL, readout->analysis, "PSPHOT.DETECTIONS");
+    if (!detections) {
+        psError(PSPHOT_ERR_CONFIG, false, "missing detections");
+        return false;
+    }
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    // peak flux is wrong : set based on previous image
+    // use the peak measured in the moments analysis:
+    for (int i = 0; i < sources->n; i++) {
+      pmSource *source = sources->data[i];
+      source->peak->flux = source->moments->Peak;
+    }
+
+    return true;
+}
+
 // generate the detection structure for the supplied array of sources
-pmDetections *psphotDetectionsFromSources (pmConfig *config, psArray *sources) {
+// XXX this currently assumes there is a single input file
+bool psphotDetectionsFromSources (pmConfig *config, const pmFPAview *view, psArray *sources) {
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", 0); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
 
     pmDetections *detections = pmDetectionsAlloc();
@@ -88,5 +247,5 @@
     float snMin = psMetadataLookupF32(NULL, recipe, "MOMENTS_SN_MIN");
     if (!isfinite(snMin)) {
-        return NULL;
+        return false;
     }
 
@@ -96,4 +255,11 @@
 
         if (source->mode & PSF_SOURCE_MASK || !isfinite(source->psfMag)) {
+            continue;
+        }
+
+        // use the existing peak information, otherwise generate a new peak
+        if (source->peak) {
+            source->peak->assigned = false; // So the moments will be measured
+            psArrayAdd (detections->peaks, 100, source->peak);
             continue;
         }
@@ -108,9 +274,9 @@
         peak->flux = flux; // this are being set wrong, but does it matter?
 
-	if (isfinite (source->errMag) && (source->errMag > 0.0)) {
-	  peak->SN = 1.0 / source->errMag; 
-	} else {
-	  peak->SN = 0.0;
-	}
+        if (isfinite (source->errMag) && (source->errMag > 0.0)) {
+          peak->SN = 1.0 / source->errMag;
+        } else {
+          peak->SN = 0.0;
+        }
 
         psArrayAdd (detections->peaks, 100, peak);
@@ -120,5 +286,11 @@
     psLogMsg ("psphot", 3, "%ld PSF sources loaded", detections->peaks->n);
 
-    return detections;
+    // save detections on the readout->analysis
+    if (!psMetadataAddPtr (readout->analysis, PS_LIST_TAIL, "PSPHOT.DETECTIONS", PS_META_REPLACE | PS_DATA_UNKNOWN, "psphot detectinos", detections)) {
+        psError (PSPHOT_ERR_CONFIG, false, "problem saving detections on readout");
+        return false;
+    }
+    psFree (detections);
+    return true;
 }
 
@@ -149,9 +321,9 @@
         peak->flux = flux; // this are being set wrong, but does it matter?
 
-	if (isfinite (source->errMag) && (source->errMag > 0.0)) {
-	  peak->SN = 1.0 / source->errMag; 
-	} else {
-	  peak->SN = 0.0;
-	}
+        if (isfinite (source->errMag) && (source->errMag > 0.0)) {
+          peak->SN = 1.0 / source->errMag;
+        } else {
+          peak->SN = 0.0;
+        }
 
         source->peak = peak;
@@ -162,2 +334,67 @@
     return true;
 }
+
+bool psphotCheckExtSources (pmConfig *config, const pmFPAview *view) {
+
+    bool status;
+
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", 0); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    // XXX allSources of newSources?
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (sources->n) {
+        // the user wants to make the psf from these stars; define them as psf stars:
+        for (int i = 0; i < sources->n; i++) {
+            pmSource *source = sources->data[i];
+            source->mode |= PM_SOURCE_MODE_PSFSTAR;
+        }
+        // force psphotChoosePSF to use all loaded sources
+        psMetadataAddS32 (recipe, PS_LIST_TAIL, "PSF_MAX_NSTARS", PS_META_REPLACE, "max number of sources for PSF model", sources->n);
+
+        // measure stats of externally specified sources
+        if (!psphotSourceStatsUpdate (sources, config, readout)) {
+            psError(PSPHOT_ERR_CONFIG, false, "failure to measure stats of existing sources");
+            return false;
+        }
+    } else {
+
+        // find the detections (by peak and/or footprint) in the image.
+        if (!psphotFindDetections (config, view, true)) {
+            psError(PSPHOT_ERR_CONFIG, false, "unable to find detections in this image");
+            return psphotReadoutCleanup (config, view);
+        }
+
+        // construct sources and measure basic stats
+        psphotSourceStats (config, view, true);
+
+        // find blended neighbors of very saturated stars
+        psphotDeblendSatstars (config, view);
+
+        // mark blended peaks PS_SOURCE_BLEND
+        if (!psphotBasicDeblend (config, view)) {
+            psLogMsg ("psphot", 3, "failed on deblend analysis");
+            return psphotReadoutCleanup (config, view);
+        }
+
+        // classify sources based on moments, brightness
+        if (!psphotRoughClass (config, view)) {
+            psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
+            return psphotReadoutCleanup (config, view);
+        }
+    }
+
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotModelBackground.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotModelBackground.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotModelBackground.c	(revision 27840)
@@ -5,5 +5,5 @@
 // Determine the binning characteristics for the background model
 // Sets the ruff image size and skip based on recipe values for the binning
-static psImageBinning *backgroundBinning(const psImage *image, // Image for which to generate a bg model
+psImageBinning *psphotBackgroundBinning(const psImage *image, // Image for which to generate a bg model
                                          const pmConfig *config // Configuration
                                          )
@@ -28,12 +28,20 @@
 }
 
+
 // Generate the background model
-static bool backgroundModel(psImage *model,  // Model image
-                            psImage *modelStdev, // Model stdev image
-                            psMetadata *analysis, // Analysis metadata for outputs
-                            pmReadout *readout, // Readout for which to generate a background model
-                            psImageBinning *binning, // Binning parameters
-                            const pmConfig *config // Configuration
-                            )
+// generate the median in NxN boxes, clipping heavily
+// linear interpolation to generate full-scale model
+//
+// NOTE that the 'analysis' metedata pass in here is used to store the binning information.
+// This may be the analysis for this readout, but it may be the analysis for the pmFPAfile
+// corresponding to the model.  Other information about the background model is saved on the
+// readout->analysis
+static bool psphotModelBackgroundReadout(psImage *model,  // Model image
+                                  psImage *modelStdev, // Model stdev image
+                                  psMetadata *analysis, // Analysis metadata for outputs
+                                  pmReadout *readout, // Readout for which to generate a background model
+                                  psImageBinning *binning, // Binning parameters
+                                  const pmConfig *config // Configuration
+    )
 {
     psTimerStart ("psphot.background");
@@ -137,6 +145,5 @@
 
     // we save the binning structure for use in psphotMagnitudes
-    psMetadataAddPtr(analysis, PS_LIST_TAIL, "PSPHOT.BACKGROUND.BINNING",
-                     PS_DATA_UNKNOWN | PS_META_REPLACE, "Background binning", binning);
+    psMetadataAddPtr(analysis, PS_LIST_TAIL, "PSPHOT.BACKGROUND.BINNING", PS_DATA_UNKNOWN | PS_META_REPLACE, "Background binning", binning);
 
     psF32 **modelData = model->data.F32;
@@ -144,4 +151,7 @@
 
     // XXXX we can thread this here by running blocks in parallel
+
+    int nFailures = 0;
+    psImageBackgroundInit();
 
     // measure clipped median for subimages
@@ -211,7 +221,10 @@
                 stats->options = PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV;
                 if (!psImageBackground(stats, &sample, subset, submask, maskVal, rng)) {
-                    psLogMsg ("psphot", PS_LOG_WARN, "Failed to estimate background using ROBUST_MEDIAN for "
-                               "(%dx%d, (row0,col0) = (%d,%d)",
-                               subset->numRows, subset->numCols, subset->row0, subset->col0);
+		    if ((nFailures < 3) || (nFailures % 100 == 0)) {
+			psLogMsg ("psphot", PS_LOG_WARN, "Failed to estimate background using ROBUST_MEDIAN for "
+				  "(%dx%d, (row0,col0) = (%d,%d)",
+				  subset->numRows, subset->numCols, subset->row0, subset->col0);
+		    }
+		    nFailures ++;
                     modelData[iy][ix] = modelStdevData[iy][ix] = NAN;
                 } else {
@@ -232,4 +245,8 @@
             psFree (submask);
         }
+    }
+
+    if (nFailures) {
+	psLogMsg ("psphot", PS_LOG_WARN, "Failed to estimate background for %d of %d subimages", nFailures, (model->numRows*model->numCols));
     }
 
@@ -293,6 +310,6 @@
     psLogMsg ("psphot", PS_LOG_INFO, "built background image: %f sec\n", psTimerMark ("psphot.background"));
 
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "SKY_MEAN", PS_META_REPLACE, "sky mean", Value);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "SKY_STDEV", PS_META_REPLACE, "sky stdev", ValueStdev);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "SKY_MEAN", PS_META_REPLACE, "sky mean", Value);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "SKY_STDEV", PS_META_REPLACE, "sky stdev", ValueStdev);
     psLogMsg ("psphot", PS_LOG_INFO, "image sky : mean %f stdev %f", Value, ValueStdev);
 
@@ -303,16 +320,10 @@
                                       PS_STAT_MAX);
     psImageStats (statsBck, model, NULL, 0);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "MSKY_MN",
-                      PS_META_REPLACE, "sky model mean",          statsBck->sampleMean);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "MSKY_SIG",
-                      PS_META_REPLACE, "sky model stdev",         statsBck->sampleStdev);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "MSKY_MAX",
-                      PS_META_REPLACE, "sky model maximum value", statsBck->max);
-    psMetadataAddF32 (recipe, PS_LIST_TAIL, "MSKY_MIN",
-                      PS_META_REPLACE, "sky model minimum value", statsBck->min);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "MSKY_NX",
-                      PS_META_REPLACE, "sky model size (x)",      model->numCols);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "MSKY_NY",
-                      PS_META_REPLACE, "sky model size (y)",      model->numRows);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "MSKY_MN", PS_META_REPLACE, "sky model mean",          statsBck->sampleMean);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "MSKY_SIG", PS_META_REPLACE, "sky model stdev",         statsBck->sampleStdev);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "MSKY_MAX", PS_META_REPLACE, "sky model maximum value", statsBck->max);
+    psMetadataAddF32 (readout->analysis, PS_LIST_TAIL, "MSKY_MIN", PS_META_REPLACE, "sky model minimum value", statsBck->min);
+    psMetadataAddS32 (readout->analysis, PS_LIST_TAIL, "MSKY_NX", PS_META_REPLACE, "sky model size (x)",      model->numCols);
+    psMetadataAddS32 (readout->analysis, PS_LIST_TAIL, "MSKY_NY", PS_META_REPLACE, "sky model size (y)",      model->numRows);
     psLogMsg ("psphot", PS_LOG_INFO, "background sky : min %f mean %f max %f stdev %f",
               statsBck->min, statsBck->sampleMean, statsBck->max, statsBck->sampleStdev);
@@ -327,17 +338,16 @@
 }
 
-
-
-psImage *psphotBackgroundModel(pmReadout *ro, const pmConfig *config)
+// generate a background model for a single readout. do not save an associated pmFPAfile for possible output
+psImage *psphotModelBackgroundReadoutNoFile(pmReadout *readout, const pmConfig *config)
 {
-    PM_ASSERT_READOUT_NON_NULL(ro, NULL);
-    PM_ASSERT_READOUT_IMAGE(ro, NULL);
+    PM_ASSERT_READOUT_NON_NULL(readout, NULL);
+    PM_ASSERT_READOUT_IMAGE(readout, NULL);
     PS_ASSERT_PTR_NON_NULL(config, NULL);
 
-    psImageBinning *binning = backgroundBinning(ro->image, config); // Image binning parameters
+    psImageBinning *binning = psphotBackgroundBinning(readout->image, config); // Image binning parameters
     psImage *model = psImageAlloc(binning->nXruff, binning->nYruff, PS_TYPE_F32); // Background model
     psImage *modelStdev = psImageAlloc(binning->nXruff, binning->nYruff, PS_TYPE_F32); // Standard deviation
 
-    if (!backgroundModel(model, modelStdev, ro->analysis, ro, binning, config)) {
+    if (!psphotModelBackgroundReadout(model, modelStdev, readout->analysis, readout, binning, config)) {
         psFree(model);
         psFree(modelStdev);
@@ -345,27 +355,24 @@
         return NULL;
     }
-
     psFree(modelStdev);
-
     return model;
 }
 
-
-// generate the median in NxN boxes, clipping heavily
-// linear interpolation to generate full-scale model
-bool psphotModelBackground (pmConfig *config, const pmFPAview *view, const char *filename)
+// generate a background model for readout number index; save an associated pmFPAfile for possible output
+bool psphotModelBackgroundReadoutFileIndex (pmConfig *config, const pmFPAview *view, const char *filename, int index)
 {
-    bool status = true;
-
     // find the currently selected readout
-    pmFPAfile *file = psMetadataLookupPtr (&status, config->files, filename);
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
     pmFPA *inFPA = file->fpa;
     pmReadout *readout = pmFPAviewThisReadout(view, inFPA);
-
-    psImageBinning *binning = backgroundBinning(readout->image, config); // Image binning parameters
-    pmReadout *model = pmFPAGenerateReadout(config, view, "PSPHOT.BACKMDL", inFPA, binning);
-    pmReadout *modelStdev = pmFPAGenerateReadout(config, view, "PSPHOT.BACKMDL.STDEV", inFPA, binning);
-
-    if (!backgroundModel(model->image, modelStdev->image, model->analysis, readout, binning, config)) {
+    psAssert (readout, "missing readout?");
+
+    psImageBinning *binning = psphotBackgroundBinning(readout->image, config); // Image binning parameters
+    pmReadout *model = pmFPAGenerateReadout(config, view, "PSPHOT.BACKMDL", inFPA, binning, index);
+    pmReadout *modelStdev = pmFPAGenerateReadout(config, view, "PSPHOT.BACKMDL.STDEV", inFPA, binning, index);
+
+    if (!psphotModelBackgroundReadout(model->image, modelStdev->image, model->analysis, readout, binning, config)) {
         psError(PS_ERR_UNKNOWN, false, "Unable to generate background model");
         return false;
@@ -375,2 +382,20 @@
     return true;
 }
+
+// XXX supply filename or keep PSPHOT.INPUT fixed?
+bool psphotModelBackground (pmConfig *config, const pmFPAview *view)
+{
+    bool status = false;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (!psphotModelBackgroundReadoutFileIndex(config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to model background for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotModelGroupInit.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotModelGroupInit.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotModelGroupInit.c	(revision 27840)
@@ -1,5 +1,5 @@
 # include "psphotInternal.h"
 
-// Add locally-defined models here.  As these mature, they can be moved to 
+// Add locally-defined models here.  As these mature, they can be moved to
 // psModule/src/objects/models
 
@@ -8,10 +8,10 @@
 
 static pmModelClass userModels[] = {
-    {"PS_MODEL_TEST1", 7, pmModelFunc_TEST1,  pmModelFlux_TEST1,  pmModelRadius_TEST1,  pmModelLimits_TEST1,  pmModelGuess_TEST1, pmModelFromPSF_TEST1, pmModelParamsFromPSF_TEST1, pmModelFitStatus_TEST1},
-    {"PS_MODEL_STRAIL", 9, pmModelFunc_STRAIL,  pmModelFlux_STRAIL,  pmModelRadius_STRAIL,  pmModelLimits_STRAIL,  pmModelGuess_STRAIL, pmModelFromPSF_STRAIL, pmModelParamsFromPSF_STRAIL, pmModelFitStatus_STRAIL},
+    {"PS_MODEL_TEST1", 7, pmModelFunc_TEST1,  pmModelFlux_TEST1,  pmModelRadius_TEST1,  pmModelLimits_TEST1,  pmModelGuess_TEST1, pmModelFromPSF_TEST1, pmModelParamsFromPSF_TEST1, pmModelFitStatus_TEST1, NULL},
+    {"PS_MODEL_STRAIL", 9, pmModelFunc_STRAIL,  pmModelFlux_STRAIL,  pmModelRadius_STRAIL,  pmModelLimits_STRAIL,  pmModelGuess_STRAIL, pmModelFromPSF_STRAIL, pmModelParamsFromPSF_STRAIL, pmModelFitStatus_STRAIL, NULL},
 };
 
-void psphotModelClassInit (void) 
-{ 
+void psphotModelClassInit (void)
+{
 
     // if pmModelClassInit returns false, we have already init'ed
@@ -20,5 +20,5 @@
     int Nmodels = sizeof (userModels) / sizeof (pmModelClass);
     for (int i = 0; i < Nmodels; i++) {
-	pmModelClassAdd (&userModels[i]);
+        pmModelClassAdd (&userModels[i]);
     }
     return;
Index: branches/simtest_nebulous_branches/psphot/src/psphotModelTest.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotModelTest.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotModelTest.c	(revision 27840)
@@ -139,5 +139,5 @@
 
     // get the source moments
-    status = pmSourceMoments (source, mRADIUS, 0.0, 1.0);
+    status = pmSourceMoments (source, mRADIUS, 0.0, 1.0, maskVal);
     if (!status) psAbort("psSourceMoments error");
     source->peak->value = source->moments->Peak;
Index: branches/simtest_nebulous_branches/psphot/src/psphotMomentsStudy.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotMomentsStudy.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotMomentsStudy.c	(revision 27840)
@@ -148,5 +148,5 @@
       pmSourceAdd(source, PM_MODEL_OP_FUNC, 0); 
 
-      pmSourceMoments (source, 32.0, sigWindow, 0.0);
+      pmSourceMoments (source, 32.0, sigWindow, 0.0, 0xffff);
       // fprintf (stderr, "sigOut : %f\n", sqrt(source->moments->Mxx));
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotOutput.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotOutput.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotOutput.c	(revision 27840)
@@ -31,4 +31,36 @@
     }
     return background;
+}
+
+// dump source stats for psf stars
+bool psphotDumpStats (psArray *sources, char *stage) {
+
+    char filename[64];
+    snprintf (filename, 64, "psf.%s.dat", stage);
+    FILE *f = fopen (filename, "w");
+    for (int i = 0; i < sources->n; i++) {
+	pmSource *source = sources->data[i];
+	if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+
+	pmModel *model = source->modelPSF;
+	if (!model) continue;
+
+	// int xc = source->peak->x - source->pixels->col0;
+	// int yc = source->peak->y - source->pixels->row0;
+	// float mcore = source->modelFlux ? source->modelFlux->data.F32[yc][xc] : NAN;
+	// float mpeak = model ? model->params->data.F32[PM_PAR_I0] : NAN;
+	// bool subtracted = source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED;
+	// fprintf (stderr, "%d %d : %d : %f %f : %f %f\n", source->peak->x, source->peak->y, subtracted, source->peak->flux, source->pixels->data.F32[yc][xc], mcore, mpeak); 
+
+	fprintf (f, "%6.1f %6.1f : %6.1f %6.1f : %8.3f %8.3f %8.3f : %f : %f %f %f : %f\n",
+		 source->peak->xf, source->peak->yf, 
+		 model->params->data.F32[PM_PAR_XPOS], model->params->data.F32[PM_PAR_YPOS], 
+		 source->psfMag, source->apMag, source->errMag,
+		 model->params->data.F32[PM_PAR_I0], 
+		 model->params->data.F32[PM_PAR_SXX], model->params->data.F32[PM_PAR_SXY], model->params->data.F32[PM_PAR_SYY], 
+		 model->params->data.F32[PM_PAR_7]);
+    }
+    fclose (f);
+    return true;
 }
 
@@ -94,16 +126,33 @@
 }
 
-bool psphotAddPhotcode (psMetadata *recipe, pmConfig *config, const pmFPAview *view, const char *filerule) {
-
-    bool status;
-
-    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, filerule);
-    PS_ASSERT (status, false);
-
-    // determine PHOTCODE from fpa & view, overwrite in recipe
-    char *photcode = pmConceptsPhotcodeForView (input, view);
+bool psphotAddPhotcode (pmConfig *config, const pmFPAview *view) {
+
+    bool status = false;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotAddPhotcodeReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to add photcode to PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotAddPhotcodeReadout (pmConfig *config, const pmFPAview *view, const char *filerule, int index) {
+
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filerule, index); // File of interest
+    PS_ASSERT (file, false);
+
+    pmReadout  *readout = pmFPAviewThisReadout (view, file->fpa);
+
+    // determine PHOTCODE from fpa & view, overwrite in readout->analysis
+    char *photcode = pmConceptsPhotcodeForView (file, view);
     PS_ASSERT (photcode, false);
 
-    psMetadataAddStr (recipe, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "photcode from FPA concepts", photcode);
+    psMetadataAddStr (readout->analysis, PS_LIST_TAIL, "PHOTCODE", PS_META_REPLACE, "photcode from FPA concepts", photcode);
     psLogMsg ("psphot", 3, "PHOTCODE is %s", photcode);
 
@@ -112,5 +161,5 @@
 }
 
-bool psphotSetHeaderNstars (psMetadata *recipe, psArray *sources) {
+bool psphotSetHeaderNstars (psMetadata *header, psArray *sources) {
 
     int nSrc = 0;
@@ -141,95 +190,93 @@
     }
 
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NSTARS",   PS_META_REPLACE, "Number of sources", nSrc);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NDET_EXT", PS_META_REPLACE, "Number of extended sources", nEXT);
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "NDET_CR",  PS_META_REPLACE, "Number of cosmic rays", nCR);
-    return true;
-}
-
-// these values are saved in an output header stub - they are added to either the
-// PHU header (CMP) or the MEF table header (CMF)
-psMetadata *psphotDefineHeader (psMetadata *recipe) {
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NSTARS",   PS_META_REPLACE, "Number of sources", nSrc);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NDET_EXT", PS_META_REPLACE, "Number of extended sources", nEXT);
+    psMetadataAddS32 (header, PS_LIST_TAIL, "NDET_CR",  PS_META_REPLACE, "Number of cosmic rays", nCR);
+    return true;
+}
+
+// these values are saved in an output header stub - they are added to either the PHU header
+// (CMP) or the MEF table header (CMF).  before the header is created, each readout has these
+// values stored on readout->analysis
+psMetadata *psphotDefineHeader (psMetadata *analysis) {
+
+    bool status = true;
 
     psMetadata *header = psMetadataAlloc ();
 
     // write necessary information to output header
-    psMetadataItemSupplement (header, recipe, "ZERO_PT");
-    psMetadataItemSupplement (header, recipe, "PHOTCODE");
-
-    psMetadataItemSupplement (header, recipe, "APMIFIT");
-    psMetadataItemSupplement (header, recipe, "DAPMIFIT");
-    psMetadataItemSupplement (header, recipe, "NAPMIFIT");
-    psMetadataItemSupplement (header, recipe, "SKYBIAS");
-    psMetadataItemSupplement (header, recipe, "SKYSAT");
+    psMetadataItemSupplement (&status, header, analysis, "ZERO_PT");
+    psMetadataItemSupplement (&status, header, analysis, "PHOTCODE");
+
+    psMetadataItemSupplement (&status, header, analysis, "APMIFIT");
+    psMetadataItemSupplement (&status, header, analysis, "DAPMIFIT");
+    psMetadataItemSupplement (&status, header, analysis, "NAPMIFIT");
 
     // PSF model parameters (shape values for image center)
-    psMetadataItemSupplement (header, recipe, "NPSFSTAR");
-    psMetadataItemSupplement (header, recipe, "APLOSS");
-
-    psMetadataItemSupplement (header, recipe, "FWHM_MAJ");
-    psMetadataItemSupplement (header, recipe, "FW_MJ_SG");
-    psMetadataItemSupplement (header, recipe, "FW_MJ_LQ");
-    psMetadataItemSupplement (header, recipe, "FW_MJ_UQ");
-
-    psMetadataItemSupplement (header, recipe, "FWHM_MIN");
-    psMetadataItemSupplement (header, recipe, "FW_MN_SG");
-    psMetadataItemSupplement (header, recipe, "FW_MN_LQ");
-    psMetadataItemSupplement (header, recipe, "FW_MN_UQ");
-
-    psMetadataItemSupplement (header, recipe, "ANGLE");
+    psMetadataItemSupplement (&status, header, analysis, "NPSFSTAR");
+    psMetadataItemSupplement (&status, header, analysis, "APLOSS");
+
+    psMetadataItemSupplement (&status, header, analysis, "FWHM_MAJ");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MJ_SG");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MJ_LQ");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MJ_UQ");
+
+    psMetadataItemSupplement (&status, header, analysis, "FWHM_MIN");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MN_SG");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MN_LQ");
+    psMetadataItemSupplement (&status, header, analysis, "FW_MN_UQ");
+
+    psMetadataItemSupplement (&status, header, analysis, "ANGLE");
 
     // Image Quality measurements
-    psMetadataItemSupplement (header, recipe, "IQ_NSTAR");
-
-    psMetadataItemSupplement (header, recipe, "IQ_FW1");
-    psMetadataItemSupplement (header, recipe, "IQ_FW1_E");
-    psMetadataItemSupplement (header, recipe, "IQ_FW2");
-    psMetadataItemSupplement (header, recipe, "IQ_FW2_E");
-
-    psMetadataItemSupplement (header, recipe, "IQ_M2");
-    psMetadataItemSupplement (header, recipe, "IQ_M2_ER");
-    psMetadataItemSupplement (header, recipe, "IQ_M2_LQ");
-    psMetadataItemSupplement (header, recipe, "IQ_M2_UQ");
-
-    psMetadataItemSupplement (header, recipe, "IQ_M2C");
-    psMetadataItemSupplement (header, recipe, "IQ_M2C_E");
-    psMetadataItemSupplement (header, recipe, "IQ_M2C_L");
-    psMetadataItemSupplement (header, recipe, "IQ_M2C_U");
-
-    psMetadataItemSupplement (header, recipe, "IQ_M2S");
-    psMetadataItemSupplement (header, recipe, "IQ_M2S_E");
-    psMetadataItemSupplement (header, recipe, "IQ_M2S_L");
-    psMetadataItemSupplement (header, recipe, "IQ_M2S_U");
-
-    psMetadataItemSupplement (header, recipe, "IQ_M3");
-    psMetadataItemSupplement (header, recipe, "IQ_M3_ER");
-    psMetadataItemSupplement (header, recipe, "IQ_M3_LQ");
-    psMetadataItemSupplement (header, recipe, "IQ_M3_UQ");
-
-    psMetadataItemSupplement (header, recipe, "IQ_M4");
-    psMetadataItemSupplement (header, recipe, "IQ_M4_ER");
-    psMetadataItemSupplement (header, recipe, "IQ_M4_LQ");
-    psMetadataItemSupplement (header, recipe, "IQ_M4_UQ");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_NSTAR");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_FW1");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_FW1_E");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_FW2");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_FW2_E");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2_ER");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2_LQ");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2_UQ");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2C");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2C_E");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2C_L");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2C_U");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2S");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2S_E");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2S_L");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M2S_U");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M3");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M3_ER");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M3_LQ");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M3_UQ");
+
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M4");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M4_ER");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M4_LQ");
+    psMetadataItemSupplement (&status, header, analysis, "IQ_M4_UQ");
 
     // XXX these need to be defined from elsewhere
     psMetadataAdd (header, PS_LIST_TAIL, "FSATUR",   PS_DATA_F32 | PS_META_REPLACE, "SATURATION MAG",      0.0);
     psMetadataAdd (header, PS_LIST_TAIL, "FLIMIT",   PS_DATA_F32 | PS_META_REPLACE, "COMPLETENESS MAG",    0.0);
-    psMetadataItemSupplement (header, recipe, "NSTARS");
-
-    psMetadataItemSupplement (header, recipe, "NDET_EXT");
-    psMetadataItemSupplement (header, recipe, "NDET_CR");
+    psMetadataItemSupplement (&status, header, analysis, "NSTARS");
+
+    psMetadataItemSupplement (&status, header, analysis, "NDET_EXT");
+    psMetadataItemSupplement (&status, header, analysis, "NDET_CR");
 
     // sky background model statistics
-    psMetadataItemSupplement (header, recipe, "MSKY_MN");
-    psMetadataItemSupplement (header, recipe, "MSKY_SIG");
-    psMetadataItemSupplement (header, recipe, "MSKY_MIN");
-    psMetadataItemSupplement (header, recipe, "MSKY_MAX");
-    psMetadataItemSupplement (header, recipe, "MSKY_NX");
-    psMetadataItemSupplement (header, recipe, "MSKY_NY");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_MN");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_SIG");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_MIN");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_MAX");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_NX");
+    psMetadataItemSupplement (&status, header, analysis, "MSKY_NY");
 
     psMetadataAddF32 (header, PS_LIST_TAIL, "DT_PHOT", PS_META_REPLACE, "elapsed psphot time", psTimerMark ("psphotReadout"));
-
-    // XXX : don't require any of these about values to exist
-    psErrorClear ();
 
     return header;
@@ -256,5 +303,5 @@
         psImageKeepCircle (source->maskObj, x, y, radius, "OR", markVal);
         pmModelSub (source->pixels, source->maskObj, source->modelPSF, PM_MODEL_OP_FULL, maskVal);
-        psImageKeepCircle (source->maskObj, x, y, radius, "AND", PS_NOT_IMAGE_MASK(markVal));
+        psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal));
     }
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotPSFConvModel.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPSFConvModel.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPSFConvModel.c	(revision 27840)
@@ -37,4 +37,8 @@
     }
 
+    // adjust the pixels based on the footprint
+    float radius = psphotSetRadiusEXT (readout, source, markVal);
+    if (!pmSourceMoments (source, radius, 0.0, 0.0, maskVal)) return false;
+
     // XXX test : modify the Io, SXX, SYY terms based on the psf SXX, SYY terms:
     psEllipseShape psfShape;
@@ -67,6 +71,4 @@
     psVector *params  = modelConv->params;
     psVector *dparams = modelConv->dparams;
-
-    psphotCheckRadiusEXT (readout, source, modelConv, markVal);
 
     // create the minimization constraints
Index: branches/simtest_nebulous_branches/psphot/src/psphotParseCamera.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotParseCamera.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotParseCamera.c	(revision 27840)
@@ -39,4 +39,6 @@
         return NULL;
     }
+    // specify the number of psphot input images
+    psMetadataAddS32 (config->arguments, PS_LIST_TAIL, "PSPHOT.INPUT.NUM", PS_META_REPLACE, "number of inputs", 1);
 
     // define the additional input/output files associated with psphot
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosian.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosian.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosian.c	(revision 27840)
@@ -1,109 +1,32 @@
 # include "psphotInternal.h"
 
-bool psphotPetrosian (pmSource *source, psMetadata *recipe, psImageMaskType maskVal) {
+bool psphotPetrosian (pmSource *source, psMetadata *recipe, float skynoise, psImageMaskType maskVal) {
 
-  bool status;
+    // XXX these need to go into recipe values
+    float Rmax = 200;
 
-  assert (source->extpars);
-  assert (source->extpars->profile);
-  assert (source->extpars->profile->radius);
-  assert (source->extpars->profile->flux);
+    psAssert (source->extpars, "need to run psphotRadialProfile first");
+    psAssert (source->extpars->ellipticalFlux, "need to run psphotRadialProfile first");
 
-  psVector *radius = source->extpars->profile->radius;
-  psVector *flux = source->extpars->profile->flux;
+    // integrate the radial profile for radial bins defined for the petrosian measurement:
+    // SB_i (r_i) where \alpha r_i < r < \beta r_i
+    if (!psphotPetrosianRadialBins (source, Rmax, skynoise)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	return false;
+    }
+  
+    // use the SB_i from above to calculate the petrosian radius and the flux within that radius
+    if (!psphotPetrosianStats (source)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	return false;
+    }
+  
+    psTrace ("psphot", 3, "source at %f,%f: petrosian radius: %f, flux: %f, axis ratio: %f, angle: %f",
+	     source->peak->xf, source->peak->yf, 
+	     source->extpars->petrosianRadius, 
+	     source->extpars->petrosianFlux, 
+	     source->extpars->axes.minor/source->extpars->axes.major, 
+	     source->extpars->axes.theta*PS_DEG_RAD);
 
-  // flux at which to measure isophotal parameters
-  float PETROSIAN_R0 = psMetadataLookupF32 (&status, recipe, "PETROSIAN_R0");
-  float PETROSIAN_RF = psMetadataLookupF32 (&status, recipe, "PETROSIAN_FLUX_RATIO");
-  assert (status);
-
-  // first find flux at R0
-  int firstAbove = -1;
-  int lastBelow = -1;
-  for (int i = 0; i < radius->n; i++) {
-    if (radius->data.F32[i] < PETROSIAN_R0) lastBelow = i;
-    if ((firstAbove < 0) && (radius->data.F32[i] > PETROSIAN_R0)) firstAbove = i;
-  }
-  // if we don't go out far enough, we have a problem...
-  if (lastBelow == radius->n - 1) {
-    psTrace ("psphot", 5, "did not go out far enough to reach petrosian reference radius...");
-    // XXX skip object? raise a flag ?
-    return false;
-  }
-  if (firstAbove < 0) {
-    psTrace ("psphot", 5, "did not go out far enough to bound petrosian reference radius");
-    // XXX raise a flag ?
-    return false;
-  }
-
-  // average flux in this range
-  float fluxR0 = 0.0;
-  int fluxRn = 0;
-  for (int i = PS_MIN(firstAbove, lastBelow); i <= PS_MAX(firstAbove, lastBelow); i++) {
-    fluxR0 += flux->data.F32[i];
-    fluxRn ++;
-  }
-  fluxR0 /= (float)(fluxRn);
-
-  // target flux for petrosian radius
-  float fluxRP = fluxR0 * PETROSIAN_RF;
-
-  // find the first bin below the flux level and the last above the level
-  // XXX can this be done faster with bisection?
-  // XXX do I need to worry about crazy outliers?
-  // XXX should i be smoothing or fitting the curve?
-  int firstBelow = -1;
-  int lastAbove = -1;
-  for (int i = 0; i < flux->n; i++) {
-    if (flux->data.F32[i] > fluxRP) lastAbove = i;
-    if ((firstBelow < 0) && (flux->data.F32[i] < fluxRP)) firstBelow = i;
-  }
-  // if we don't go out far enough, we have a problem...
-  if (lastAbove == radius->n - 1) {
-    psTrace ("psphot", 5, "did not go out far enough to reach petrosian radius...");
-    // XXX skip object? raise a flag ?
-    return false;
-  }
-  if (firstBelow < 0) {
-    psTrace ("psphot", 5, "did not go out far enough to bound petrosian radius");
-    // XXX raise a flag ?
-    return false;
-  }
-
-  // need to examine pixels in this vicinity
-  float fluxFirst = 0;
-  float fluxLast = 0;
-  for (int i = 0; i <= PS_MAX(firstBelow, lastAbove); i++) {
-    if (i <= firstBelow) {
-      fluxFirst += flux->data.F32[i];
-    }
-    if (i <= lastAbove) {
-      fluxLast += flux->data.F32[i];
-    }
-  }
-  float fluxRPSum    = 0.5*(fluxLast + fluxFirst);
-  float fluxRPSumErr = 0.5*fabs(fluxLast - fluxFirst);
-  // XXX need to use the weight appropriately here...
-
-  float rad     = 0.5*(radius->data.F32[firstBelow] + radius->data.F32[lastAbove]);
-  float radErr  = 0.5*fabs(radius->data.F32[firstBelow] - radius->data.F32[lastAbove]);
-
-  if (!source->extpars->petrosian) {
-    source->extpars->petrosian = pmSourcePetrosianValuesAlloc ();
-  }
-
-  // these are uncalibrated: instrumental mags and pixel units
-  source->extpars->petrosian->mag    = -2.5*log10(fluxRPSum);
-  source->extpars->petrosian->magErr = fluxRPSumErr / fluxRPSum;
-
-  source->extpars->petrosian->rad    = rad;
-  source->extpars->petrosian->radErr = radErr;
-
-  psTrace ("psphot", 5, "Petrosian flux:%f +/- %f @ %f +/- %f for %f, %f\n",
-           source->extpars->petrosian->mag, source->extpars->petrosian->magErr,
-           source->extpars->petrosian->rad, source->extpars->petrosian->radErr,
-           source->peak->xf, source->peak->yf);
-
-  return true;
-
+    return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianAnalysis.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianAnalysis.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianAnalysis.c	(revision 27840)
@@ -0,0 +1,68 @@
+# include "psphotInternal.h"
+
+// aperture-like measurements for extended sources
+bool psphotPetrosianAnalysis (pmReadout *readout, psArray *sources, psMetadata *recipe) {
+
+    bool status;
+
+    // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
+    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
+    assert (maskVal);
+
+    // XXX temporary user-supplied systematic sky noise measurement (derive from background model)
+    float skynoise = psMetadataLookupF32 (&status, recipe, "SKY.NOISE");
+
+# if (0)
+    // if backModel or backStdev are missing, the values of sky and/or skyErr will be set to NAN
+    // XXX use this to set skynoise
+    pmReadout *backModel = psphotSelectBackground (config, view);
+    pmReadout *backStdev = psphotSelectBackgroundStdev (config, view);
+# endif
+
+    // S/N limit to perform full non-linear fits
+    float SN_LIM = psMetadataLookupF32 (&status, recipe, "EXTENDED_SOURCE_SN_LIM");
+
+    // option to limit analysis to a specific region
+    char *region = psMetadataLookupStr (&status, recipe, "ANALYSIS_REGION");
+    psRegion AnalysisRegion = psRegionForImage (readout->image, psRegionFromString (region));
+    if (psRegionIsNaN (AnalysisRegion)) psAbort("analysis region mis-defined");
+
+    // source analysis is done in S/N order (brightest first)
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // choose the sources of interest
+    for (int i = 0; i < sources->n; i++) {
+
+	pmSource *source = sources->data[i];
+
+	// skip PSF-like and non-astronomical objects
+	if (source->type == PM_SOURCE_TYPE_STAR) continue;
+	if (source->type == PM_SOURCE_TYPE_DEFECT) continue;
+	if (source->type == PM_SOURCE_TYPE_SATURATED) continue;
+	if (source->mode & PM_SOURCE_MODE_DEFECT) continue;
+	if (source->mode & PM_SOURCE_MODE_SATSTAR) continue;
+	if (!(source->mode & PM_SOURCE_MODE_EXT_LIMIT)) continue;
+
+	// limit selection to some SN limit
+	assert (source->peak); // how can a source not have a peak?
+	if (source->peak->SN < SN_LIM) continue;
+
+	// limit selection by analysis region
+	if (source->peak->x < AnalysisRegion.x0) continue;
+	if (source->peak->y < AnalysisRegion.y0) continue;
+	if (source->peak->x > AnalysisRegion.x1) continue;
+	if (source->peak->y > AnalysisRegion.y1) continue;
+
+	// replace object in image
+	if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+	    pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+	}
+
+	psphotPetrosianProfile (readout, source, skynoise);
+
+	pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
+    }
+
+    psphotVisualShowResidualImage (readout);
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianProfile.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianProfile.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianProfile.c	(revision 27840)
@@ -0,0 +1,80 @@
+# include "psphotInternal.h"
+
+// generate the Petrosian radius and flux using elliptical contours
+
+// XXX much of this function is focused on generating the clean contours, which can be used by 
+// any number of aperture-like measurements.  probably will want to rename the pmPetrosian
+// structure to something the pmRadialProfile
+
+bool psphotPetrosianProfile (pmReadout *readout, pmSource *source, float skynoise) {
+
+    // container to hold results from the radial profile analysis
+    pmPetrosian *petrosian = pmPetrosianAlloc();
+
+    // XXX these need to go into recipe values
+    int Nsec = 24;
+    float Rmax = 200;
+    float fluxMin = 0.0;
+    float fluxMax = source->peak->flux;
+
+    // generate a series of radial profiles at Nsec evenly spaced angles.  the profile flux
+    // is measured by interpolation for small radii; for large radii, the pixels in a box
+    // are averaged to increase the S/N (XXX not yet done)
+    if (!psphotRadialProfilesByAngles (source, petrosian, Nsec, Rmax)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to measure radial profile for petrosian");
+	psFree (petrosian);
+	return false;
+    }
+
+    // use the radial profiles to determine the radius of a given isophote.  this isophote
+    // is used to determine the elliptical shape of the object, so it has a relatively high
+    // value (nominally 50% of the peak)
+    if (!psphotRadiiFromProfiles (source, petrosian, fluxMin, fluxMax)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to measure isophotal radii from profiles");
+	psFree (petrosian);
+	return false;
+    }
+
+    // convert the isophotal radius vs angle measurements to an elliptical contour
+    if (!psphotEllipticalContour (source, petrosian)) {
+	psLogMsg ("psphot", 3, "failed to measure elliptical contour");
+	psFree (petrosian);
+	return false;
+    }
+  
+    // generate a single, normalized radial profile following the elliptical contours.
+    // the radius is normalized by the axis ratio so that on the major axis, 1 pixel = 1 pixel
+    if (!psphotEllipticalProfile (source, petrosian)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	psFree (petrosian);
+	return false;
+    }
+  
+    // integrate the radial profile for radial bins defined for the petrosian measurement:
+    // SB_i (r_i) where \alpha r_i < r < \beta r_i
+    if (!psphotPetrosianRadialBins (source, petrosian, Rmax, skynoise)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	psFree (petrosian);
+	return false;
+    }
+  
+    // use the SB_i from above to calculate the petrosian radius and the flux within that radius
+    if (!psphotPetrosianStats (source, petrosian)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	psFree (petrosian);
+	return false;
+    }
+  
+    // XXX this will only work in the psphot context, not the psphotPetrosianStudy...
+    // XXX add the petrosian to the pmSource structure...
+    // psphotVisualShowResidualImage (readout);
+    psphotVisualShowPetrosian (source, petrosian);
+
+    psphotPetrosianFreeVectors(petrosian);
+
+    psTrace ("psphot", 3, "source at %f,%f: petrosian radius: %f, flux: %f, axis ratio: %f, angle: %f",
+	     source->peak->xf, source->peak->yf, petrosian->petrosianRadius, petrosian->petrosianFlux, petrosian->axes.minor/petrosian->axes.major, PS_DEG_RAD*petrosian->axes.theta);
+
+    psFree (petrosian);
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianRadialBins.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianRadialBins.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianRadialBins.c	(revision 27840)
@@ -0,0 +1,229 @@
+# include "psphotInternal.h"
+float InterpolateValues (float X0, float Y0, float X1, float Y1, float X);
+
+// convert the flux vs elliptical radius to annular bins
+
+// we are guaranteed to be limited by either the seeing (1 - few pixels) or by the pixels
+// themselves.  this function does not attempt to measure the radial profiles accurately
+// for radii that are smaller than ~2 pixels
+
+// for small radii, we are measuring the mean surface brightness in non-overlapping radial
+// bins.  for large radii (r > 2 pixels), we are measuring the surface brightness for a
+// radius range \alpha r_i < i < \beta r_i, but performing this measurement for radii more
+// finely spaced than r_{i+1} = r_i * \beta / \alpha.  for the integration, we need to
+// track the non-overlapping radius values.
+
+// Photo interpolates the image of interest to place the peak on the center of the central
+// pixel, and then uses the exact fractions of the pixels in each of the first few annuli.
+// Seems like a reasonable thing, but is there any significance to the difference?
+
+// XXX move the resulting elements from profile to extpars->petrosian?
+bool psphotPetrosianRadialBins (pmSource *source, float radiusMax, float skynoise) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->extpars->ellipticalFlux, "missing ellipticalFlux");
+
+    psVector *radius = source->extpars->ellipticalFlux->radiusElliptical;
+    psVector *flux = source->extpars->ellipticalFlux->fluxElliptical;
+
+    // sort incoming vectors by radius
+    pmSourceRadialProfileSortPair (radius, flux);
+
+    if (!source->extpars->petProfile) {
+	source->extpars->petProfile = pmSourceRadialProfileAlloc();
+    }
+    pmSourceRadialProfile *profile = source->extpars->petProfile;
+
+    float skyModelErrorSQ = PS_SQR(skynoise);
+
+    int nMax = radiusMax;
+
+    // radBin stores the centers of the radial bins, 
+    // radMin, radMax store the bounds
+    psVector *radMin  = psVectorAllocEmpty(nMax, PS_TYPE_F32);
+    psVector *radMax  = psVectorAllocEmpty(nMax, PS_TYPE_F32);
+    psVector *radAlp  = psVectorAllocEmpty(nMax, PS_TYPE_F32);
+    psVector *radBet  = psVectorAllocEmpty(nMax, PS_TYPE_F32);
+
+    psVector *binSB      = psVectorAllocEmpty(nMax, PS_TYPE_F32); // surface brightness of radial bin
+    psVector *binSBstdev = psVectorAllocEmpty(nMax, PS_TYPE_F32); // surface brightness of radial bin
+    psVector *binRad  	 = psVectorAllocEmpty(nMax, PS_TYPE_F32); // mean radius of radial bin
+    psVector *binArea 	 = psVectorAllocEmpty(nMax, PS_TYPE_F32); // area of radial bin (contiguous, non-overlapping)
+
+    psVectorInit (binSB, 0.0);
+    psVectorInit (binSBstdev, 0.0);
+    psVectorInit (binRad, 0.0);
+
+    // generate radial bin bounds
+    radMin->data.F32[0] = 0.0;
+    radMax->data.F32[0] = 1.0;
+    radAlp->data.F32[0] = 0.0;
+    radBet->data.F32[0] = 1.0;
+    
+    radMin->data.F32[1] = 1.0;
+    radMax->data.F32[1] = 1.5;
+    radAlp->data.F32[1] = 1.0;
+    radBet->data.F32[1] = 1.5;
+    
+    radMin->data.F32[2] = 1.5;
+    radMax->data.F32[2] = 2.0;
+    radAlp->data.F32[2] = 1.5;
+    radBet->data.F32[2] = 2.0;
+    
+# define PETROSIAN_ALPHA 0.8
+# define PETROSIAN_BETA 1.25
+# define POWER_LAW_SPACING true
+    
+    // power-law spacing with overlapping boundaries at the geometric mid-points
+    float rBeta = sqrt(PETROSIAN_BETA);
+    for (int i = 3; radBet->data.F32[i-1] < radiusMax; i++) {
+	if (POWER_LAW_SPACING) {
+	    radMin->data.F32[i] = radMax->data.F32[i-1];
+	    radMax->data.F32[i] = radMin->data.F32[i] * PETROSIAN_BETA;
+	    radAlp->data.F32[i] = radMin->data.F32[i] / rBeta;
+	    radBet->data.F32[i] = radMax->data.F32[i] * rBeta;
+	} else {
+	    radMin->data.F32[i] = radMax->data.F32[i-1];
+	    radMax->data.F32[i] = radMin->data.F32[i] + 1;
+	    float rMid = 0.5*(radMin->data.F32[i] + radMax->data.F32[i]);
+	    radAlp->data.F32[i] = rMid * PETROSIAN_ALPHA;
+	    radBet->data.F32[i] = rMid * PETROSIAN_BETA;
+	}
+	radMin->n = radMax->n = radAlp->n = radBet->n = i + 1;
+    }
+
+    // generate radial area-weighted mean radius & non-overlapping areas
+    for (int i = 0; i < radMin->n; i++) {
+	float rMin = radMin->data.F32[i];
+	float rMax = radMax->data.F32[i];
+	
+	float rMin2 = rMin*rMin;
+	float rMin3 = rMin2*rMin;
+
+	float rMax2 = rMax*rMax;
+	float rMax3 = rMax2*rMax;
+
+	float rBin = 2.0 * (rMax3 - rMin3) / (rMax2 - rMin2) / 3.0;
+	
+	// XXX calculate area-weighted radius rather than asserting?
+	binRad->data.F32[i] = rBin;
+	binArea->data.F32[i] = M_PI * (rMax2 - rMin2);
+
+	psTrace ("psphot", 6, "%3d  %5.1f %5.1f : %5.1f : %5.1f %5.1f\n", 
+		 i, radAlp->data.F32[i], radMin->data.F32[i], binRad->data.F32[i],
+		 radMax->data.F32[i], radBet->data.F32[i]);
+    }
+
+    // storage vector for stats
+    psVector *values = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    bool done = false;
+    int nOut = 0;
+    float Rmin = radAlp->data.F32[nOut];
+    float Rmax = radBet->data.F32[nOut];
+    float Rnxt = radAlp->data.F32[nOut+1];  // minimum radius for next range
+    int iNext = 0;
+    for (int i = 0; !done && (i < radius->n); i++) {
+	if (radius->data.F32[i] < Rnxt) {
+	  iNext = i;
+	}
+	if (radius->data.F32[i] > Rmax) {
+	    // calculate the value for the nOut bin
+	    float value, dvalue;
+	    if (values->n > 0) {
+		psVectorStats (stats, values, NULL, NULL, 0);
+		value = stats->robustMedian;
+		dvalue = stats->robustStdev;
+	    } else {
+		value = NAN;
+		dvalue = NAN;
+	    }
+
+	    binSB->data.F32[nOut] = value;
+	    binSBstdev->data.F32[nOut] = sqrt(PS_SQR(dvalue) / values->n + skyModelErrorSQ);
+
+	    // error in the SB is the stdev per bin / sqrt (number of pixels) 
+	    // added in quadrature to a fraction of the local sky (not the 
+	    // residual flux, but the sky from the sky model)
+
+	    psTrace ("psphot", 5, "%3d  %5.1f %5.1f : %5.1f  %5.2f\n", nOut, radAlp->data.F32[nOut], radBet->data.F32[nOut], binSB->data.F32[nOut], binSBstdev->data.F32[nOut]);
+
+	    nOut ++;
+	    if (nOut >= radAlp->n) break;
+	    Rmin = radAlp->data.F32[nOut];
+	    Rmax = radBet->data.F32[nOut];
+	    Rnxt = (nOut < nMax - 1) ? radAlp->data.F32[nOut+1] : Rmax;  // minimum radius for next range
+	    values->n = 0;
+	    psStatsInit(stats);
+	    i = iNext;
+	}
+	if (radius->data.F32[i] < Rmin) {
+	    continue;
+	}
+	psVectorAppend (values, flux->data.F32[i]);
+    }
+    binSB->n = binSBstdev->n = binRad->n = binArea->n = nOut;
+    // XXX I think this misses the last radial bin -- do we care?
+
+    // interpolate any bins that were empty (extrapolate to center if needed)
+    if (!isfinite(binSB->data.F32[0]) && !isfinite(binSB->data.F32[1])) {
+	psWarning ("center 2 bins of source at %f, %f are NAN, skipping this source", source->peak->xf, source->peak->yf);
+	// XXX raise a flag
+	psFree(binSB);
+	psFree(binSBstdev);
+	psFree(binRad);
+	psFree(binArea);
+	psFree(radMin);
+	psFree(radMax);
+	psFree(radAlp);
+	psFree(radBet);
+	psFree(values);
+	psFree(stats);
+	return false;
+    }
+
+    // if center bin is empty assume same SB as next radius (probably true due to PSF)
+    if (!isfinite(binSB->data.F32[0])) {
+	binSB->data.F32[0] = binSB->data.F32[1];
+	binSBstdev->data.F32[0] = binSBstdev->data.F32[1];
+    }
+
+    // interpolate any bins that were empty (if center if needed)
+    for (int i = 1; i < binSB->n - 1; i++) {
+	if (isfinite(binSB->data.F32[i])) continue;
+	binSB->data.F32[i] = InterpolateValues (binRad->data.F32[i-1], binSB->data.F32[i-1], binRad->data.F32[i+1], binSB->data.F32[i+1], binRad->data.F32[i]);
+	binSBstdev->data.F32[i] = InterpolateValues (binRad->data.F32[i-1], binSBstdev->data.F32[i-1], binRad->data.F32[i+1], binSBstdev->data.F32[i+1], binRad->data.F32[i]);
+    }
+
+    psFree(profile->binSB);
+    psFree(profile->binSBstdev);
+    psFree(profile->radialBins);
+    psFree(profile->area);
+
+    // save the vectors
+    profile->radialBins = binRad;
+    profile->area       = binArea;
+    profile->binSB      = binSB;
+    profile->binSBstdev = binSBstdev;
+
+    // psphotPetrosianVisualProfileRadii (radius, flux, binRad, binSB, source->peak->flux, 0.0);
+
+    psFree(radMin);
+    psFree(radMax);
+    psFree(radAlp);
+    psFree(radBet);
+    psFree(values);
+    psFree(stats);
+
+    return true;
+}
+
+// the area-weighted mean radius is given by:
+
+// integral r * 2 pi r dr / integral 2 pi r dr
+
+// = 2/3 pi (r_max^3 - r_min^3)  / pi (r_max^2 - r_min^2) 
+// = 2/3 (r_max^3 - r_min^3) / (r_max^2 - r_min^2)
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStats.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStats.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStats.c	(revision 27840)
@@ -0,0 +1,210 @@
+# include "psphotInternal.h"
+
+# define PETROSIAN_RATIO 0.2
+# define PETROSIAN_RADII 2.0
+
+// generate the Petrosian radius and flux from the mean surface brightness (r_i)
+
+float InterpolateValues (float X0, float Y0, float X1, float Y1, float X);
+
+bool psphotPetrosianStats (pmSource *source) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->extpars->petProfile, "missing petProfile");
+
+    pmSourceRadialProfile *profile = source->extpars->petProfile;
+
+    psVector *binSB      = profile->binSB;
+    psVector *binSBstdev = profile->binSBstdev;
+    psVector *binRad     = profile->radialBins;
+    psVector *area       = profile->area;
+
+    psVector *fluxSum     = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *fluxSumErr2 = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *refRadius   = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *petRatio    = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *petRatioErr = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *meanSB      = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+    psVector *areaSum     = psVectorAllocEmpty(binSB->n, PS_TYPE_F32);
+
+    float petRadius = NAN;
+    float petFlux = NAN;
+
+    bool anyPetro = false;
+    bool manyPetro = false;
+    bool above = true;
+    float Asum = 0.0;
+    float Fsum = 0.0;
+    float dFsum2 = 0.0;
+
+    float nSigma = 3.0;
+    int lowestSignificantRadius = 0;
+    float lowestSignificantRatio = 1.0;
+
+    // find the Petrosian Radius and Petrosian Flux
+
+    int nOut = 0;
+    for (int i = 0; i < binSB->n; i++) {
+	// skip nan bins (do not contribute to flux or area)
+	if (!isfinite(binSB->data.F32[i])) continue;
+
+	float Area = area->data.F32[i];
+	Asum += Area;
+	Fsum += binSB->data.F32[i] * Area;
+	dFsum2 += PS_SQR(binSBstdev->data.F32[i] * Area);
+
+	float areaInner = 0.5 * Area;
+	float fluxInner = 0.5 * Area * binSB->data.F32[i];
+	float fluxInnerErr2 = PS_SQR(binSBstdev->data.F32[i] * 0.5 * Area);
+	if (nOut > 0) {
+	    areaInner += areaSum->data.F32[nOut-1];
+	    fluxInner += fluxSum->data.F32[nOut-1];
+	    fluxInnerErr2 += fluxSumErr2->data.F32[nOut-1];
+	}
+
+	// ratio = binSB / meanSB
+	// meanSB = flux / area
+	// flux = sum(binSB(i) * area(i)
+	// fluxErr^2 = sum(binSBerr(i)^2 area(i)^2)
+	// meanSBerr^2 = fluxErr^2 / area^2
+	// (ratioErr/ratio)^2 = (binSBerr/binSB)^2 + (meanSBerr/meanSB)^2
+
+	psVectorAppend(meanSB, (fluxInner / areaInner));
+
+	float ratio = binSB->data.F32[i] / meanSB->data.F32[nOut];
+	psVectorAppend(petRatio, ratio);
+
+	float meanSBerr = sqrt(fluxInnerErr2) / areaInner;
+	float ratioErr = fabs(ratio) * sqrt(PS_SQR(binSBstdev->data.F32[i]/binSB->data.F32[i]) + PS_SQR(meanSBerr/meanSB->data.F32[nOut]));
+
+	psVectorAppend(petRatioErr, ratioErr);
+
+	psVectorAppend(areaSum, Asum);
+	psVectorAppend(fluxSum, Fsum);
+	psVectorAppend(fluxSumErr2, dFsum2);
+	psVectorAppend(refRadius, binRad->data.F32[i]);
+
+	psTrace ("psphot", 4, "%3d : %5.2f : %5.3f %5.3f : %5.3f %5.3f : %5.3f %5.3f : %5.3f %5.3f : %5.1f %5.1f\n", 
+		 i, refRadius->data.F32[nOut], 
+		 binSB->data.F32[i], binSBstdev->data.F32[i], 
+		 meanSB->data.F32[nOut], meanSBerr, 
+		 petRatio->data.F32[nOut], petRatioErr->data.F32[nOut], 
+		 fluxSum->data.F32[nOut], sqrt(fluxSumErr2->data.F32[nOut]), areaSum->data.F32[nOut], areaInner);
+    
+	// anytime we transition below the PETROSIAN_RATIO, calculate the radius and flux
+	// we will keep and report the last (largest radius) value
+	if (above && (petRatio->data.F32[nOut] < PETROSIAN_RATIO) && (petRatio->data.F32[nOut] > nSigma*petRatioErr->data.F32[nOut])) {
+	    // interpolate Rvec between i-1 and i to PETROSIAN_RATIO to get flux (Fvec) and radius (rvec)
+	    if (i == 0) { 
+		// assume Fmax @ R = 0.0
+		petRadius = InterpolateValues (1.0, 0.0, petRatio->data.F32[nOut], refRadius->data.F32[nOut], PETROSIAN_RATIO);
+	    } else {
+		petRadius = InterpolateValues (petRatio->data.F32[nOut-1], refRadius->data.F32[nOut-1], petRatio->data.F32[nOut], refRadius->data.F32[nOut], PETROSIAN_RATIO);
+	    }
+	    above = false;
+	    if (anyPetro) manyPetro = true;
+	    anyPetro = true;
+	}
+    
+	// anytime we transition below the PETROSIAN_RATIO, calculate the radius and flux
+	// we will keep and report the last (largest radius) value
+	// find the last signficant measurement of the petrosian ratio
+	if (above && (petRatio->data.F32[nOut] < lowestSignificantRatio) && (petRatio->data.F32[nOut] > nSigma*petRatioErr->data.F32[nOut])) {
+	    lowestSignificantRadius = nOut;
+	    lowestSignificantRatio = petRatio->data.F32[nOut];
+	}
+    
+	// reset on transitions up, but do not re-calculate rad_90, flux_90
+	if (!above && (petRatio->data.F32[nOut] >= PETROSIAN_RATIO)) {
+	    above = true;
+	}
+	nOut ++;
+    }
+
+    if (!anyPetro) {
+	// interpolate Rvec between i-1 and i to PETROSIAN_RATIO to get flux (Fvec) and radius (rvec)
+	if (lowestSignificantRadius == 0) { 
+	    // assume Fmax @ R = 0.0
+	    petRadius = InterpolateValues (1.0, 0.0, petRatio->data.F32[lowestSignificantRadius], refRadius->data.F32[lowestSignificantRadius], PETROSIAN_RATIO);
+	} else {
+	    petRadius = InterpolateValues (petRatio->data.F32[lowestSignificantRadius-1], refRadius->data.F32[lowestSignificantRadius-1], petRatio->data.F32[lowestSignificantRadius], refRadius->data.F32[lowestSignificantRadius], PETROSIAN_RATIO);
+	}
+    }
+
+    // now measure the flux within PETROSIAN_RADII * petRadius 
+    float apRadius = PETROSIAN_RADII * petRadius;
+    for (int i = 0; i < refRadius->n; i++) {
+	// XXX use bisection to do this faster:
+	if (refRadius->data.F32[i] > apRadius) {
+	    if (i == 0) {
+		psWarning ("does this case make any sense? (refRadius[0] > apRadius)");
+		continue;
+	    } else {
+		petFlux = InterpolateValues (refRadius->data.F32[i-1], fluxSum->data.F32[i-1], refRadius->data.F32[i], fluxSum->data.F32[i], apRadius);
+		break;
+	    }
+	}
+    }
+
+    // now measure the radii R90 and R50 where flux = 0.9 (or 0.5) * petFlux;
+    float flux90 = 0.9 * petFlux;
+    float flux50 = 0.5 * petFlux;
+    float R50 = NAN;
+    float R90 = NAN;
+    bool found50 = false;
+    bool found90 = false;
+    // XXX use bisection to do this faster:
+    for (int i = 0; !(found50 && found90) && i < refRadius->n; i++) {
+	if (!found50 && (fluxSum->data.F32[i] > flux50)) {
+	    if (i == 0) {
+		psWarning ("does this case make any sense? (fluxSum[0] > flux50)");
+		continue;
+	    } else {
+		R50 = InterpolateValues (fluxSum->data.F32[i-1], refRadius->data.F32[i-1], fluxSum->data.F32[i], refRadius->data.F32[i], flux50);
+		found50 = true;
+	    }
+	}
+	if (!found90 && (fluxSum->data.F32[i] > flux90)) {
+	    if (i == 0) {
+		psWarning ("does this case make any sense? (fluxSum[0] > flux90)");
+		continue;
+	    } else {
+		R90 = InterpolateValues (fluxSum->data.F32[i-1], refRadius->data.F32[i-1], fluxSum->data.F32[i], refRadius->data.F32[i], flux90);
+		found90 = true;
+	    }
+	}
+    }
+
+
+    // XXX save flags (anyPetro, manyPetro)
+    source->extpars->petrosianRadius = petRadius;
+    source->extpars->petrosianFlux   = petFlux;
+    source->extpars->petrosianR50    = R50;
+    source->extpars->petrosianR90    = R90;
+    
+    // XXX add the errors
+    source->extpars->petrosianRadiusErr = NAN;
+    source->extpars->petrosianFluxErr   = NAN;
+    source->extpars->petrosianR50Err    = NAN;
+    source->extpars->petrosianR90Err    = NAN;
+
+    fprintf (stderr, "source @ %f,%f\n", source->peak->xf, source->peak->yf);
+    psphotPetrosianVisualStats (binRad, binSB, refRadius, meanSB, petRatio, petRatioErr, fluxSum, petRadius, PETROSIAN_RATIO, petFlux, apRadius);
+
+    psFree(fluxSum);
+    psFree(fluxSumErr2);
+    psFree(refRadius);
+    psFree(petRatio);
+    psFree(petRatioErr);
+    psFree(meanSB);
+    psFree(areaSum);
+
+    return true;
+}
+
+float InterpolateValues (float X0, float Y0, float X1, float Y1, float X) {
+    float Y = Y0 + (Y1 - Y0) * (X - X0) / (X1 - X0);
+    return Y;
+}
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStudy.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStudy.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianStudy.c	(revision 27840)
@@ -0,0 +1,184 @@
+# include "psphotInternal.h"
+
+# define DX 512
+# define DY 512
+
+// XXX add noise and seeing.
+// XXX double check on sersic functional form
+// XXX modify ratio if ratio > 1.0 (swap major and minor)
+
+pmPeak *psphotLocalPeak(pmReadout *readout, int Xo, int Yo);
+
+int main (int argc, char **argv) {
+
+  pmErrorRegister();                  // register psModule's error codes/messages
+  pmModelClassInit();
+
+  int N;
+  float Xo = 0.5*DX;
+  float Yo = 0.5*DY;
+  char *image = NULL;
+  pmSource *source = NULL;
+
+  float peak = 1000.0;
+  float sigma = 2.0;	      // major axis size 
+  float ARatio = 1.0;
+  float angle = 0.0;
+  float sersic = 0.5;
+  float skynoise = 0.0;
+  
+  if ((N = psArgumentGet (argc, argv, "-peak"))) {
+    psArgumentRemove (N, &argc, argv);
+    peak = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-sigma"))) {
+    psArgumentRemove (N, &argc, argv);
+    sigma = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-aratio"))) {
+    psArgumentRemove (N, &argc, argv);
+    ARatio = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-angle"))) {
+    psArgumentRemove (N, &argc, argv);
+    angle = PS_RAD_DEG*atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-sersic"))) {
+    psArgumentRemove (N, &argc, argv);
+    sersic = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-skynoise"))) {
+    psArgumentRemove (N, &argc, argv);
+    skynoise = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-visual"))) {
+    psArgumentRemove (N, &argc, argv);
+    pmVisualSetVisual(true);
+  }
+  if ((N = psArgumentGet (argc, argv, "-coords"))) {
+    psArgumentRemove (N, &argc, argv);
+    Xo = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+    Yo = atof(argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+  if ((N = psArgumentGet (argc, argv, "-image"))) {
+    psArgumentRemove (N, &argc, argv);
+    image = psStringCopy (argv[N]);
+    psArgumentRemove (N, &argc, argv);
+  }
+
+  if (argc != 1) {
+    fprintf (stderr, "USAGE: psphotPetrosianStudy\n");
+    exit (2);
+  }
+
+  // create a containing image & associated readout
+  pmReadout *readout = pmReadoutAlloc(NULL);
+  if (!image) {
+      readout->image = psImageAlloc(DX, DY, PS_TYPE_F32);
+
+      // create a dummy variance, but don't populate -- it is not used by pmSourceMoments if the sigma parameters is set to 0.0
+      readout->variance = psImageAlloc(DX, DY, PS_TYPE_F32);
+
+      // create a model & associated source
+      pmModelType type = pmModelClassGetType("PS_MODEL_SERSIC");
+      pmModel *model = pmModelAlloc(type);
+
+      // set the model parameters
+      model->params->data.F32[PM_PAR_SKY]  = 0.0;
+      model->params->data.F32[PM_PAR_I0]   = peak;
+      model->params->data.F32[PM_PAR_XPOS] = Xo;
+      model->params->data.F32[PM_PAR_YPOS] = Yo;
+
+      psEllipseAxes axes;
+      axes.major = sigma;
+      axes.minor = sigma*ARatio;
+      axes.theta = angle;
+
+      psEllipseShape shape = psEllipseAxesToShape (axes);
+
+      // XXX set the sigma with user input
+      model->params->data.F32[PM_PAR_SXX]  = shape.sx * M_SQRT2;
+      model->params->data.F32[PM_PAR_SYY]  = shape.sy * M_SQRT2;
+      model->params->data.F32[PM_PAR_SXY]  = shape.sxy;
+
+      if (model->params->n > 7) {
+	  model->params->data.F32[PM_PAR_7]  = sersic;
+      }
+
+      // generate source container & populate image
+      source = pmSourceFromModel(model, readout, Xo, PM_SOURCE_TYPE_STAR);
+
+      // generate the modelFlux 
+      pmSourceCacheModel(source, 0);
+
+      // instantiate the source
+      pmSourceAdd(source, PM_MODEL_OP_FUNC, 0); 
+
+      // XXX add noise here...
+      psphotSaveImage(NULL, readout->image, "sersic.fits");
+
+  } else {
+      psRegion full = psRegionSet(0,0,0,0);
+      psFits *fits = psFitsOpen(image, "r");
+      readout->image = psFitsReadImage(fits, full, 0);
+
+      source = pmSourceAlloc();
+      source->peak = psphotLocalPeak(readout, Xo, Yo);
+      pmSourceDefinePixels (source, readout, Xo, Yo, 128);
+  }
+
+  psphotPetrosianProfile (readout, source, skynoise);
+
+  psFree (source);
+
+  exit (0);
+}
+
+// Xo, Yo are in parent coords
+pmPeak *psphotLocalPeak(pmReadout *readout, int Xo, int Yo) {
+
+    int Xp = Xo;
+    int Yp = Yo;
+    float peakFlux = readout->image->data.F32[Yp][Xp];
+
+    // find local peak within +/- 3 pix of the given coordinate
+    for (int iy = Yo - 3; iy <= Yo + 3; iy++) {
+	for (int ix = Xo - 3; ix <= Xo + 3; ix++) {
+	    if (peakFlux < readout->image->data.F32[iy][ix]) {
+		Xp = ix;
+		Yp = iy;
+		peakFlux = readout->image->data.F32[Yp][Xp];
+	    }
+	}
+    }
+
+    pmPeak *peak = pmPeakAlloc(Xp, Yp, peakFlux, PM_PEAK_LONE);
+
+    // calculate fractional peak position relative to Xp,Yp
+    psPolynomial2D *bicube = psImageBicubeFit (readout->image, Xp, Yp);
+    psPlane min = psImageBicubeMin (bicube);
+    psFree (bicube);
+
+    // if min point is too deviant, use the peak value
+    if ((fabs(min.x) < 1.5) && (fabs(min.y) < 1.5)) {
+        peak->xf = min.x + Xp;
+        peak->yf = min.y + Yp;
+
+	// xf,yf must land on image with 0 pixel border
+	peak->xf = PS_MAX (PS_MIN (peak->xf, readout->image->numCols - 1), readout->image->col0);
+	peak->yf = PS_MAX (PS_MIN (peak->yf, readout->image->numRows - 1), readout->image->row0);
+    } else {
+        peak->xf = Xp;
+        peak->yf = Yp;
+    }
+
+    return peak;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotPetrosianVisual.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotPetrosianVisual.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotPetrosianVisual.c	(revision 27840)
@@ -0,0 +1,406 @@
+# include "psphotInternal.h"
+# define FORCE_VISUAL 0
+
+// this function displays representative images as the psphot analysis progresses:
+// 0 : image, 1 : variance
+// 0 : backsub, 1 : variance, 2 : backgnd
+// 0 : backsub, 1 : variance, 2 : signif
+// (overlay peaks on images)
+// (overlay footprints on images)
+// (overlay moments on images)
+// (overlay rough class on images)
+// 0 : backsub, 1 : psfpos, 2: psfsub
+// 0 : backsub, 1 : lin_resid, 2: psfsub
+
+# if (HAVE_KAPA)
+# include <kapa.h>
+
+// functions used to visualize the analysis as it goes
+// these are invoked by the -visual options
+
+static int kapa = -1;
+static int kapa2 = -1;
+
+// if no valid data is supplied (NULL or n <- 0), leave limits as they were
+bool pmVisualLimitsFromVectors (Graphdata *graphdata, psVector *xVec, psVector *yVec) {
+
+    if (xVec && xVec->n > 0) {
+	graphdata->xmin = graphdata->xmax = xVec->data.F32[0];
+	for (int i = 1; i < xVec->n; i++) {
+	    if (!isfinite(xVec->data.F32[i])) continue;
+	    graphdata->xmin = PS_MIN (graphdata->xmin, xVec->data.F32[i]);
+	    graphdata->xmax = PS_MAX (graphdata->xmax, xVec->data.F32[i]);
+	}
+	float range = graphdata->xmax - graphdata->xmin;
+	graphdata->xmax += 0.05*range;
+	graphdata->xmin -= 0.05*range;
+    }
+    if (yVec && yVec->n > 0) {
+	graphdata->ymin = graphdata->ymax = yVec->data.F32[0];
+	for (int i = 1; i < yVec->n; i++) {
+	    if (!isfinite(yVec->data.F32[i])) continue;
+	    graphdata->ymin = PS_MIN (graphdata->ymin, yVec->data.F32[i]);
+	    graphdata->ymax = PS_MAX (graphdata->ymax, yVec->data.F32[i]);
+	}
+	float range = graphdata->ymax - graphdata->ymin;
+	graphdata->ymax += 0.05*range;
+	graphdata->ymin -= 0.05*range;
+    }
+    return true;
+}
+
+bool psphotPetrosianVisualProfileByAngle (psVector *radius, psVector *flux) {
+
+    Graphdata graphdata;
+
+    // return true;
+    if (!FORCE_VISUAL && !pmVisualIsVisual()) return true;
+
+    if (kapa2 == -1) {
+        kapa2 = KapaOpenNamedSocket ("kapa", "psphot:plots");
+        if (kapa2 == -1) {
+            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+            pmVisualSetVisual(false);
+            return false;
+        }
+    }
+
+    KapaClearPlots (kapa2);
+    KapaInitGraph (&graphdata);
+    KapaSetFont (kapa2, "courier", 14);
+
+    pmVisualLimitsFromVectors (&graphdata, radius, flux);
+    KapaSetLimits (kapa2, &graphdata);
+
+    KapaBox (kapa2, &graphdata);
+    KapaSendLabel (kapa2, "radius", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "flux", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot (kapa2, radius->n, &graphdata);
+    KapaPlotVector (kapa2, radius->n, radius->data.F32, "x");
+    KapaPlotVector (kapa2, radius->n, flux->data.F32, "y");
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10];
+    fprintf (stdout, "[c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    }
+    return true;
+}
+
+bool psphotPetrosianVisualProfileRadii (psVector *radius, psVector *flux, psVector *radiusBin, psVector *fluxBin, float peakFlux, float RadiusRef) {
+
+    float FluxRef = 500.0;
+
+    Graphdata graphdata;
+
+    // return true;
+    if (!FORCE_VISUAL && !pmVisualIsVisual()) return true;
+
+    if (kapa == -1) {
+        kapa = KapaOpenNamedSocket ("kapa", "psphot:plots");
+        if (kapa == -1) {
+            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+            pmVisualSetVisual(false);
+            return false;
+        }
+    }
+
+    KapaClearPlots (kapa);
+    KapaInitGraph (&graphdata);
+    KapaSetFont (kapa, "courier", 14);
+
+    graphdata.ymax = +1.05*peakFlux;
+    graphdata.ymin = -0.05*peakFlux;
+    pmVisualLimitsFromVectors (&graphdata, radius, NULL);
+    KapaSetLimits (kapa, &graphdata);
+
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "radius", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "flux", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot (kapa, radius->n, &graphdata);
+    KapaPlotVector (kapa, radius->n, radius->data.F32, "x");
+    KapaPlotVector (kapa, radius->n, flux->data.F32, "y");
+
+    // do this with log-r, log-flux?
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.style = 2;
+    graphdata.ptype = 2;
+    graphdata.size = 2.0;
+    KapaPrepPlot   (kapa, radiusBin->n, &graphdata);
+    KapaPlotVector (kapa, radiusBin->n, radiusBin->data.F32, "x");
+    KapaPlotVector (kapa, radiusBin->n, fluxBin->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = 2;
+    graphdata.ptype = 7;
+    graphdata.size = 3.0;
+    KapaPrepPlot (kapa, 1, &graphdata);
+    KapaPlotVector (kapa, 1, &RadiusRef, "x");
+    KapaPlotVector (kapa, 1, &FluxRef, "y");
+
+    fprintf (stderr, "radius: %f\n", RadiusRef);
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10];
+    fprintf (stdout, "[c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    }
+    return true;
+}
+
+bool psphotPetrosianVisualStats (psVector *radBin, psVector *fluxBin, 
+				 psVector *refRadius, psVector *meanSB, 
+				 psVector *petRatio, psVector *petRatioErr,
+				 psVector *fluxSum, 
+				 float petRadius, float ratioForRadius,
+				 float petFlux, float radiusForFlux)
+{
+    Graphdata graphdata;
+    KapaSection section;
+
+    if (!FORCE_VISUAL && !pmVisualIsVisual()) return true;
+
+    if (kapa2 == -1) {
+        kapa2 = KapaOpenNamedSocket ("kapa", "psphot:stats");
+        if (kapa2 == -1) {
+            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+            pmVisualSetVisual(false);
+            return false;
+        }
+    }
+
+    KapaClearPlots (kapa2);
+    KapaInitGraph (&graphdata);
+    KapaSetFont (kapa2, "courier", 14);
+
+    // radius vs flux
+    // radius vs mean SB
+    // radius vs petRatio
+
+    // *** section 1: radius vs mean SB
+    section.dx = 1.00;
+    section.dy = 0.33;
+    section.x  = 0.00;
+    section.y  = 0.00;
+    section.name = psStringCopy ("meanSB");
+    KapaSetSection (kapa2, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    pmVisualLimitsFromVectors (&graphdata, radBin, fluxBin);
+    KapaSetLimits (kapa2, &graphdata);
+
+    KapaBox (kapa2, &graphdata);
+    KapaSendLabel (kapa2, "radius", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "mean SB", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot (kapa2, radBin->n, &graphdata);
+    KapaPlotVector (kapa2, radBin->n, radBin->data.F32, "x");
+    KapaPlotVector (kapa2, radBin->n, fluxBin->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = 2;
+    graphdata.ptype = 1;
+    graphdata.size = 2.0;
+    KapaPrepPlot (kapa2, refRadius->n, &graphdata);
+    KapaPlotVector (kapa2, refRadius->n, refRadius->data.F32, "x");
+    KapaPlotVector (kapa2, refRadius->n, meanSB->data.F32, "y");
+
+    // *** section 2: radius vs petrosian ratio
+    section.dx = 1.00;
+    section.dy = 0.33;
+    section.x  = 0.00;
+    section.y  = 0.33;
+    section.name = psStringCopy ("ratio");
+    KapaSetSection (kapa2, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ymax = +1.05;
+    graphdata.ymin = -0.05;
+    pmVisualLimitsFromVectors (&graphdata, radBin, NULL);
+    KapaSetLimits (kapa2, &graphdata);
+
+    KapaBox (kapa2, &graphdata);
+    KapaSendLabel (kapa2, "radius", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "ratio", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    graphdata.etype = 0x01;
+    KapaPrepPlot (kapa2, refRadius->n, &graphdata);
+    KapaPlotVector (kapa2, refRadius->n, refRadius->data.F32, "x");
+    KapaPlotVector (kapa2, refRadius->n, petRatio->data.F32, "y");
+    KapaPlotVector (kapa2, refRadius->n, petRatioErr->data.F32, "dym");
+    KapaPlotVector (kapa2, refRadius->n, petRatioErr->data.F32, "dyp");
+    graphdata.etype = 0;
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = 2;
+    graphdata.ptype = 2;
+    graphdata.size = 2.0;
+    KapaPrepPlot   (kapa2, 1, &graphdata);
+    KapaPlotVector (kapa2, 1, &petRadius, "x");
+    KapaPlotVector (kapa2, 1, &ratioForRadius, "y");
+
+    // *** section 3: radius vs integrated flux
+    section.dx = 1.00;
+    section.dy = 0.33;
+    section.x  = 0.00;
+    section.y  = 0.66;
+    section.name = psStringCopy ("flux");
+    KapaSetSection (kapa2, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    pmVisualLimitsFromVectors (&graphdata, radBin, fluxSum);
+    KapaSetLimits (kapa2, &graphdata);
+
+    KapaBox (kapa2, &graphdata);
+    KapaSendLabel (kapa2, "radius", KAPA_LABEL_XM);
+    KapaSendLabel (kapa2, "integrated flux", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 2;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot   (kapa2, refRadius->n, &graphdata);
+    KapaPlotVector (kapa2, refRadius->n, refRadius->data.F32, "x");
+    KapaPlotVector (kapa2, refRadius->n, fluxSum->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 2;
+    graphdata.style = 2;
+    graphdata.size = 2.0;
+    KapaPrepPlot   (kapa2, 1, &graphdata);
+    KapaPlotVector (kapa2, 1, &radiusForFlux, "x");
+    KapaPlotVector (kapa2, 1, &petFlux, "y");
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10];
+    fprintf (stdout, "[c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    }
+    return true;
+}
+
+bool psphotPetrosianVisualEllipticalContour (pmSourceRadialFlux *radFlux, pmSourceExtendedPars *extpars) {
+
+    Graphdata graphdata;
+
+    if (!FORCE_VISUAL && !pmVisualIsVisual()) return true;
+
+    if (kapa == -1) {
+        kapa = KapaOpenNamedSocket ("kapa", "psphot:plots");
+        if (kapa == -1) {
+            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+            pmVisualSetVisual(false);
+            return false;
+        }
+    }
+
+    KapaClearPlots (kapa);
+    KapaInitGraph (&graphdata);
+    KapaSetFont (kapa, "courier", 14);
+
+    psVector *theta = radFlux->theta;
+    psVector *radius = radFlux->isophotalRadii;
+
+    // find Rmin and Rmax for the initial guess
+    float Rmin = radius->data.F32[0];
+    float Rmax = radius->data.F32[0];
+
+    psVector *Rx = psVectorAlloc(radius->n, PS_TYPE_F32);
+    psVector *Ry = psVectorAlloc(radius->n, PS_TYPE_F32);
+
+    for (int i = 0; i < theta->n; i++) {
+	Rx->data.F32[i] = radius->data.F32[i]*cos(theta->data.F32[i]);
+	Ry->data.F32[i] = radius->data.F32[i]*sin(theta->data.F32[i]);
+
+	// check the radius range
+	Rmin = MIN (Rmin, radius->data.F32[i]);
+	Rmax = MAX (Rmax, radius->data.F32[i]);
+    }	
+
+    psVector *rx = psVectorAlloc(361, PS_TYPE_F32);
+    psVector *ry = psVectorAlloc(361, PS_TYPE_F32);
+
+    float epsilon = extpars->axes.minor / extpars->axes.major;
+
+    for (int i = 0; i < 361; i++) {
+
+	float alpha = PS_RAD_DEG * i;
+
+	float cs_alpha = cos(alpha);
+	float sn_alpha = sin(alpha);
+
+	float cs_phi = cos(alpha - extpars->axes.theta);
+	float sn_phi = sin(alpha - extpars->axes.theta);
+
+	float r = 1.0 / sqrt(SQ(sn_phi) + SQ(epsilon*cs_phi));
+
+	// generate the model fit here
+	rx->data.F32[i] = extpars->axes.minor * cs_alpha * r;
+	ry->data.F32[i] = extpars->axes.minor * sn_alpha * r;
+    }	
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = -1.1*Rmax;
+    graphdata.ymin = -1.1*Rmax;
+    graphdata.xmax = +1.1*Rmax;
+    graphdata.ymax = +1.1*Rmax;
+    KapaSetLimits (kapa, &graphdata);
+
+    KapaBox (kapa, &graphdata);
+    KapaSendLabel (kapa, "R_x", KAPA_LABEL_XM);
+    KapaSendLabel (kapa, "R_y", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.style = 2;
+    graphdata.ptype = 2;
+    graphdata.size = 1.0;
+    KapaPrepPlot (kapa, Rx->n, &graphdata);
+    KapaPlotVector (kapa, Rx->n, Rx->data.F32, "x");
+    KapaPlotVector (kapa, Rx->n, Ry->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.style = 0;
+    graphdata.ptype = 0;
+    graphdata.size = 1.0;
+    KapaPrepPlot (kapa, rx->n, &graphdata);
+    KapaPlotVector (kapa, rx->n, rx->data.F32, "x");
+    KapaPlotVector (kapa, rx->n, ry->data.F32, "y");
+
+    // pause and wait for user input:
+    // continue, save (provide name), ??
+    char key[10];
+    fprintf (stdout, "[c]ontinue? ");
+    if (!fgets(key, 8, stdin)) {
+        psWarning("Unable to read option");
+    }
+    return true;
+}
+
+# endif
Index: branches/simtest_nebulous_branches/psphot/src/psphotRadialBins.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRadialBins.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRadialBins.c	(revision 27840)
@@ -0,0 +1,204 @@
+# include "psphotInternal.h"
+float InterpolateValues (float X0, float Y0, float X1, float Y1, float X);
+
+// convert the flux vs elliptical radius to annular bins
+
+// we are guaranteed to be limited by either the seeing (1 - few pixels) or by the pixels
+// themselves.  this function does not attempt to measure the radial profiles accurately
+// for radii that are smaller than ~2 pixels
+
+// for small radii, we are measuring the mean surface brightness in non-overlapping radial
+// bins.  for large radii (r > 2 pixels), we are measuring the surface brightness for a
+// radius range \alpha r_i < i < \beta r_i, but performing this measurement for radii more
+// finely spaced than r_{i+1} = r_i * \beta / \alpha.  for the integration, we need to
+// track the non-overlapping radius values.
+
+// Photo interpolates the image of interest to place the peak on the center of the central
+// pixel, and then uses the exact fractions of the pixels in each of the first few annuli.
+// Seems like a reasonable thing, but is there any significance to the difference?
+
+// XXX move the resulting elements from profile to extpars->petrosian?
+bool psphotRadialBins (psMetadata *recipe, pmSource *source, float radiusMax, float skynoise) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->extpars->ellipticalFlux, "missing ellipticalFlux");
+
+    psVector *radius = source->extpars->ellipticalFlux->radiusElliptical;
+    psVector *flux = source->extpars->ellipticalFlux->fluxElliptical;
+
+    // sort incoming vectors by radius
+    pmSourceRadialProfileSortPair (radius, flux);
+
+    if (!source->extpars->radProfile) {
+	source->extpars->radProfile = pmSourceRadialProfileAlloc();
+    }
+    pmSourceRadialProfile *profile = source->extpars->radProfile;
+
+    float skyModelErrorSQ = PS_SQR(skynoise);
+    psEllipseAxes axes = source->extpars->axes;
+    float AxialRatio = axes.minor / axes.major;
+
+    // radMin, radMax store the bounds of the annuli
+    bool status = false;
+    psVector *radMin = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.LOWER");
+    psVector *radMax = psMetadataLookupPtr (&status, recipe, "RADIAL.ANNULAR.BINS.UPPER");
+    if (!radMin || !radMin->n) return false;
+    if (!radMax || !radMax->n) return false;
+
+    psVector *binSB      = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // surface brightness of radial bin
+    psVector *binSBstdev = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // surface brightness error of radial bin
+    psVector *binSum     = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // surface brightness of radial bin
+    psVector *binFill    = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // surface brightness of radial bin
+    psVector *binRad  	 = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // mean radius of radial bin
+    psVector *binArea 	 = psVectorAllocEmpty(radMin->n, PS_TYPE_F32); // area of radial bin (contiguous, non-overlapping)
+
+    psVectorInit (binSB, 0.0);
+    psVectorInit (binSBstdev, 0.0);
+    psVectorInit (binSum, 0.0);
+    psVectorInit (binFill, 0.0);
+
+    psVectorInit (binRad, 0.0);
+
+    // generate radial area-weighted mean radius & non-overlapping areas
+    for (int i = 0; i < radMin->n; i++) {
+	float rMin2 = PS_SQR(radMin->data.F32[i]);
+	float rMax2 = PS_SQR(radMax->data.F32[i]);
+
+	float rMin3 = rMin2*radMin->data.F32[i];
+	float rMax3 = rMax2*radMax->data.F32[i];
+
+	float rBin = 2.0 * (rMax3 - rMin3) / (rMax2 - rMin2) / 3.0;
+	
+	// XXX calculate area-weighted radius rather than asserting?
+	binRad->data.F32[i] = rBin;
+	binArea->data.F32[i] = M_PI * (rMax2 - rMin2);
+    }
+
+    // storage vector for stats
+    psVector *values = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
+
+    float fluxSum = 0.0;
+    int nPixSum = 0;
+
+    bool done = false;
+    int nOut = 0;
+    float Rmin = radMin->data.F32[nOut];
+    float Rmax = radMax->data.F32[nOut];
+    float Rnxt = radMin->data.F32[nOut+1];  // minimum radius for next range
+    int iNext = 0;
+    int iLast = -1;
+    for (int i = 0; !done && (i < radius->n); i++) {
+	if (radius->data.F32[i] < Rnxt) {
+	    iNext = i; // on the next pass, we will start back here to ensure we hit all pixels in the next bin
+	}
+	if (radius->data.F32[i] > Rmax) {
+	    // calculate the surface brightness for bin 'nOut'
+	    float value, dvalue;
+	    if (values->n > 0) {
+		psVectorStats (stats, values, NULL, NULL, 0);
+		value = stats->robustMedian;
+		dvalue = stats->robustStdev;
+	    } else {
+		value = NAN;
+		dvalue = NAN;
+	    }
+
+	    binSB->data.F32[nOut] = value;
+	    binSBstdev->data.F32[nOut] = sqrt(PS_SQR(dvalue) / values->n + skyModelErrorSQ);
+
+	    // calculate the total flux for bin 'nOut'
+	    float Area = M_PI*AxialRatio*PS_SQR(Rmax);
+	    binSum->data.F32[nOut] = fluxSum;
+	    binFill->data.F32[nOut] = nPixSum / Area;
+
+	    psTrace ("psphot", 5, "radial bins: %3d  %5.1f %5.1f : %7.1f  %6.2f : %8.1f %4.2f %6.1f\n", 
+		     nOut, radMin->data.F32[nOut], radMax->data.F32[nOut], 
+		     binSB->data.F32[nOut], binSBstdev->data.F32[nOut], 
+		     binSum->data.F32[nOut], binFill->data.F32[nOut], Area);
+
+	    nOut ++;
+	    if (nOut >= radMin->n) break;
+	    Rmin = radMin->data.F32[nOut];
+	    Rmax = radMax->data.F32[nOut];
+	    Rnxt = (nOut < radMin->n - 1) ? radMin->data.F32[nOut+1] : Rmax;  // minimum radius for next range
+	    values->n = 0;
+	    psStatsInit(stats);
+	    iLast = i;
+	    i = iNext;
+	}
+	if (radius->data.F32[i] < Rmin) {
+	    continue;
+	}
+	psVectorAppend (values, flux->data.F32[i]);
+
+	if (i > iLast) {
+	    fluxSum += flux->data.F32[i];
+	    nPixSum ++;
+	}
+    }
+    binSB->n = binSBstdev->n = binRad->n = binArea->n = nOut;
+
+    // interpolate any bins that were empty (extrapolate to center if needed)
+    if (!isfinite(binSB->data.F32[0]) && !isfinite(binSB->data.F32[1])) {
+	psWarning ("center 2 bins of source at %f, %f are NAN, skipping this source", source->peak->xf, source->peak->yf);
+	// XXX raise a flag
+	psFree(binSB);
+	psFree(binSBstdev);
+
+	psFree(binSum);
+	psFree(binFill);
+
+	psFree(binRad);
+	psFree(binArea);
+	psFree(values);
+	psFree(stats);
+	return false;
+    }
+
+    // if center bin is empty assume same SB as next radius (probably true due to PSF)
+    if (!isfinite(binSB->data.F32[0])) {
+	binSB->data.F32[0] = binSB->data.F32[1];
+	binSBstdev->data.F32[0] = binSBstdev->data.F32[1];
+    }
+
+    // interpolate any bins that were empty (if center if needed)
+    for (int i = 1; i < binSB->n - 1; i++) {
+	if (isfinite(binSB->data.F32[i])) continue;
+	binSB->data.F32[i] = InterpolateValues (binRad->data.F32[i-1], binSB->data.F32[i-1], binRad->data.F32[i+1], binSB->data.F32[i+1], binRad->data.F32[i]);
+	binSBstdev->data.F32[i] = InterpolateValues (binRad->data.F32[i-1], binSBstdev->data.F32[i-1], binRad->data.F32[i+1], binSBstdev->data.F32[i+1], binRad->data.F32[i]);
+    }
+
+    psFree(profile->binSB);
+    psFree(profile->binSBstdev);
+
+    psFree(profile->binSum);
+    psFree(profile->binFill);
+
+    psFree(profile->radialBins);
+    psFree(profile->area);
+
+    // save the vectors
+    profile->binSB      = binSB;
+    profile->binSBstdev = binSBstdev;
+
+    profile->binSum     = binSum;
+    profile->binFill    = binFill;
+
+    profile->radialBins = binRad;
+    profile->area       = binArea;
+
+    psFree(values);
+    psFree(stats);
+
+    return true;
+}
+
+// the area-weighted mean radius is given by:
+
+// integral r * 2 pi r dr / integral 2 pi r dr
+
+// = 2/3 pi (r_max^3 - r_min^3)  / pi (r_max^2 - r_min^2) 
+// = 2/3 (r_max^3 - r_min^3) / (r_max^2 - r_min^2)
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotRadialProfile.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRadialProfile.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRadialProfile.c	(revision 27840)
@@ -1,21 +1,7 @@
 # include "psphotInternal.h"
 
-# define COMPARE_RADIUS(A,B) (radius->data.F32[A] < radius->data.F32[B])
-# define SWAP_RADIUS(TYPE,A,B) { \
-  float tmp; \
-  if (A != B) { \
-    tmp = radius->data.F32[A]; \
-    radius->data.F32[A] = radius->data.F32[B]; \
-    radius->data.F32[B] = tmp; \
-    tmp = flux->data.F32[A]; \
-    flux->data.F32[A] = flux->data.F32[B]; \
-    flux->data.F32[B] = tmp; \
-    tmp = variance->data.F32[A]; \
-    variance->data.F32[A] = variance->data.F32[B]; \
-    variance->data.F32[B] = tmp; \
-  } \
-}
+bool psphotRadialProfile (pmSource *source, psMetadata *recipe, float skynoise, psImageMaskType maskVal) {
 
-bool psphotRadialProfile (pmSource *source, psMetadata *recipe, psImageMaskType maskVal) {
+    bool status;
 
     // allocate pmSourceExtendedParameters, if not already defined
@@ -24,48 +10,47 @@
     }
 
-    if (!source->extpars->profile) {
-        source->extpars->profile = pmSourceRadialProfileAlloc ();
+    // XXX these need to go into recipe values
+    int Nsec = 24;
+    float Rmax = 200;
+    float fluxMin = 0.0;
+    float fluxMax = source->peak->flux;
+
+    bool RAW_RADIUS = psMetadataLookupBool (&status, recipe, "EXTENDED_SOURCE_RAW_RADIUS");
+
+    // generate a series of radial profiles at Nsec evenly spaced angles.  the profile flux
+    // is measured by interpolation for small radii; for large radii, the pixels in a box
+    // are averaged to increase the S/N
+    if (!psphotRadialProfilesByAngles (source, Nsec, Rmax)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to measure radial profile for petrosian");
+	return false;
     }
 
-    int nPts = source->pixels->numRows * source->pixels->numCols;
-    source->extpars->profile->radius = psVectorAllocEmpty (nPts, PS_TYPE_F32);
-    source->extpars->profile->flux   = psVectorAllocEmpty (nPts, PS_TYPE_F32);
-    source->extpars->profile->variance = psVectorAllocEmpty (nPts, PS_TYPE_F32);
+    // use the radial profiles to determine the radius of a given isophote.  this isophote
+    // is used to determine the elliptical shape of the object, so it has a relatively high
+    // value (nominally 25% of the peak)
+    if (!psphotRadiiFromProfiles (source, fluxMin, fluxMax)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to measure isophotal radii from profiles");
+	return false;
+    }
 
-    psVector *radius = source->extpars->profile->radius;
-    psVector *flux   = source->extpars->profile->flux;
-    psVector *variance = source->extpars->profile->variance;
-
-    // XXX use the extended source model here for Xo, Yo?
-    // XXX define a radius scaled to the elliptical contour?
-
-    int n = 0;
-
-    float Xo = 0.0;
-    float Yo = 0.0;
-
-    if (source->modelEXT) {
-      Xo = source->modelEXT->params->data.F32[PM_PAR_XPOS] - source->pixels->col0;
-      Yo = source->modelEXT->params->data.F32[PM_PAR_YPOS] - source->pixels->row0;
-    } else {
-      Xo = source->peak->xf - source->pixels->col0;
-      Yo = source->peak->yf - source->pixels->row0;
+    // convert the isophotal radius vs angle measurements to an elliptical contour
+    if (!psphotEllipticalContour (source)) {
+	psLogMsg ("psphot", 3, "failed to measure elliptical contour");
+	return false;
     }
-    for (int iy = 0; iy < source->pixels->numRows; iy++) {
-        for (int ix = 0; ix < source->pixels->numCols; ix++) {
-            if (source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix]) continue;
-            radius->data.F32[n] = hypot (ix - Xo, iy - Yo) ;
-            flux->data.F32[n]   = source->pixels->data.F32[iy][ix];
-            variance->data.F32[n] = source->variance->data.F32[iy][ix];
-            n++;
-        }
+  
+    // generate a single, normalized radial profile following the elliptical contours.
+    // the radius is normalized by the axis ratio so that on the major axis, 1 pixel = 1 pixel
+    if (!psphotEllipticalProfile (source, RAW_RADIUS)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate elliptical profile");
+	return false;
     }
-    radius->n = n;
-    variance->n = n;
-    flux->n = n;
-
-    // sort the vector set by the radius
-    PSSORT (radius->n, COMPARE_RADIUS, SWAP_RADIUS, NONE);
-
+  
+    // generated profile in averaged bins
+    if (!psphotRadialBins (recipe, source, Rmax, skynoise)) {
+	psError (PS_ERR_UNKNOWN, false, "failed to generate radial bins");
+	return false;
+    }
+  
     return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotRadialProfileByAngles.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRadialProfileByAngles.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRadialProfileByAngles.c	(revision 27840)
@@ -0,0 +1,243 @@
+# include "psphotInternal.h"
+
+// Given a source at (x,y), generate a collection of radial profiles at even angular separations
+
+// These functions are used to calculate the stats in a rectangle at arbitrary orientation.
+// XXX Move these elsewhere (psLib?)
+float psphotMeanSectorValue (psImage *image, float x, float y, float dL, float dW, float theta);
+psVector *psphotBoxValues (psImage *image, float x0, float y0, float dL, float dW, float theta);
+psVector *psphotLineValues (psImage *image, double x1, double y1, double x2, double y2, int dW);
+psVector *psphotLineValuesBresen (psImage *image, int X1, int Y1, int X2, int Y2, int dW, int swapcoords);
+
+bool psphotRadialProfilesByAngles (pmSource *source, int Nsec, float Rmax) {
+
+    psAssert (source->extpars, "define extpars");
+
+    // we want to have an even number of sectors so we can do 180 deg symmetrizing
+    Nsec = (Nsec % 2) ? Nsec + 1 : Nsec;
+    float dtheta = 2.0*M_PI / Nsec;
+
+    if (!source->extpars->radFlux) {
+	source->extpars->radFlux = pmSourceRadialFluxAlloc();
+    }
+    pmSourceRadialFlux *profile = source->extpars->radFlux;
+    psFree(profile->radii);
+    psFree(profile->fluxes);
+    psFree(profile->theta);
+
+    profile->radii = psArrayAllocEmpty(Nsec);
+    profile->fluxes = psArrayAllocEmpty(Nsec);
+    profile->theta = psVectorAllocEmpty(Nsec, PS_TYPE_F32);
+
+    for (int i = 0; i < Nsec; i++) {
+
+	float theta = i*dtheta;
+
+	psVector *radius = psVectorAllocEmpty(Rmax, PS_TYPE_F32);
+	psVector *flux   = psVectorAllocEmpty(Rmax, PS_TYPE_F32);
+
+	// Start at Xo,Yo and find the x,y locations for r_i, theta where r_i initially
+	// increments by 1 pixel.  At large radii (r*dtheta > 2) use stats in a box rather than
+	// sub-pixel interpolation
+
+	int dR = 1.0;
+	for (float r = 0; r < Rmax; r += dR) {
+
+	    float Xo = source->peak->xf;
+	    float Yo = source->peak->yf;
+
+	    // Xo,Yo are referenced to pixels with bounds i+0.0, i+1.0
+	    float x = r * cos (theta) + Xo;
+	    float y = r * sin (theta) + Yo;
+	    dR = 2*(int)(0.5*r*sin(dtheta)) + 1;
+
+	    if (x < 0) goto badvalue;
+	    if (y < 0) goto badvalue;
+	    if (x >= source->pixels->parent->numCols) goto badvalue;
+	    if (y >= source->pixels->parent->numRows) goto badvalue;
+
+	    float value = NAN;
+	    if (dR < 2) {
+		// value is NAN if we run off the image
+		// 0.5 PIX: this function takes pixel coords; source peak is in pixel coords
+		value = psImageInterpolatePixelBilinear(x, y, source->pixels);
+	    } else {
+		// 0.5 PIX: this function takes pixel coords; source peak is in pixel coords
+		value = psphotMeanSectorValue(source->pixels, x, y, dR, dR, theta);
+	    }
+
+	    // keep the all values (even NAN) so all vectors are matched in length
+	    psVectorAppend (radius, r);
+	    psVectorAppend (flux, value);
+	    continue;
+	    
+	badvalue:
+	    psVectorAppend (radius, r);
+	    psVectorAppend (flux, NAN);
+	}
+
+	psArrayAdd (profile->radii, 100, radius);
+	psArrayAdd (profile->fluxes, 100, flux);
+	psVectorAppend (profile->theta, theta);
+
+	// psphotPetrosianVisualProfileByAngle (radius, flux);
+
+	psFree(radius);
+	psFree(flux);
+    }
+
+    for (int i = 0; i < Nsec / 2; i++) {
+
+	psVector *r1 = profile->radii->data[i];
+	psVector *r2 = profile->radii->data[i+Nsec/2];
+
+	psVector *f1 = profile->fluxes->data[i];
+	psVector *f2 = profile->fluxes->data[i+Nsec/2];
+
+	psAssert (r1->n == r2->n, "mis-matched vectors");
+	psAssert (f1->n == f2->n, "mis-matched vectors");
+
+	// we have a pair of vectors i, i+Nsec/2; replace them with the finite minimum of the pair
+	for (int j = 0; j < r1->n; j++) {
+	    
+	    float flux;
+
+	    if (!isfinite(f1->data.F32[j]) && !isfinite(f2->data.F32[j])) {
+		flux = NAN;
+		goto setflux;
+	    }
+
+	    if (!isfinite(f1->data.F32[j])) {
+		flux = f2->data.F32[j];
+		goto setflux;
+	    }
+	    if (!isfinite(f2->data.F32[j])) {
+		flux = f1->data.F32[j];
+		goto setflux;
+	    }
+
+	    flux = PS_MIN(f1->data.F32[j], f2->data.F32[j]);
+
+	setflux:
+	    f1->data.F32[j] = flux;
+	    f2->data.F32[j] = flux;
+	}
+    }    
+    return true;
+}
+
+float psphotMeanSectorValue (psImage *image, float x, float y, float dL, float dW, float theta) {
+
+    psVector *values = psphotBoxValues (image, x, y, dL, dW, theta);
+    if (!values) goto escape;
+    if (!values->n) goto escape;
+    
+    psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+    psVectorStats (stats, values, NULL, NULL, 0);
+
+    float value = stats->sampleMedian;
+
+    psFree (stats);
+    psFree (values);
+    
+    return value;
+
+escape:
+    psFree(values);
+    return NAN;    
+}
+
+psVector *psphotBoxValues (psImage *image, float x0, float y0, float dL, float dW, float theta) {
+
+    // extract pixels from a series of lines (from -0.5*dW to +0.5*dW) of length dL, 
+    // centered on x0, y0 in parent pixel coordinates (not pixel indicies)
+
+    float xs = x0 - image->col0 - 0.5*dL*cos(theta);
+    float ys = y0 - image->row0 - 0.5*dL*sin(theta);
+
+    float xe = xs + 0.5*dL*cos(theta);
+    float ye = ys + 0.5*dL*sin(theta);
+
+    psVector *values = psphotLineValues (image, xs, ys, xe, ye, (int) dW);
+    return values;
+}
+
+/**
+ * identify the quadrant and draw the correct line
+ */
+psVector *psphotLineValues (psImage *image, double x1, double y1, double x2, double y2, int dW) {
+
+  int FlipDirect, FlipCoords;
+  int X1, Y1, X2, Y2, dX, dY;
+
+  /* rather than draw the line from float positions, we find the closest
+     integer end-points and draw the line between those pixels */
+
+  X1 = ROUND(x1);
+  Y1 = ROUND(y1);
+  X2 = ROUND(x2);
+  Y2 = ROUND(y2);
+
+  dX = X2 - X1;
+  dY = Y2 - Y1;
+
+  FlipCoords = (abs(dX) < abs(dY));
+  FlipDirect = FlipCoords ? (y1 > y2) : (x1 > x2);
+
+  psVector *values = NULL;
+  if (!FlipDirect && !FlipCoords) values = psphotLineValuesBresen (image, X1, Y1, X2, Y2, dW, FALSE);
+  if ( FlipDirect && !FlipCoords) values = psphotLineValuesBresen (image, X2, Y2, X1, Y1, dW, FALSE);
+  if (!FlipDirect &&  FlipCoords) values = psphotLineValuesBresen (image, Y1, X1, Y2, X2, dW, TRUE);
+  if ( FlipDirect &&  FlipCoords) values = psphotLineValuesBresen (image, Y2, X2, Y1, X1, dW, TRUE);
+
+  return values;
+}
+
+/**
+ * use the Bresenham line drawing technique
+ * integer-only Bresenham line-draw version which is fast
+ */
+psVector *psphotLineValuesBresen (psImage *image, int X1, int Y1, int X2, int Y2, int dW, int swapcoords) {
+
+    int X, Y, dX, dY;
+    int e, e2;
+
+    psVector *values = psVectorAllocEmpty(100, PS_TYPE_F32);
+
+    dX = X2 - X1;
+    dY = Y2 - Y1;
+
+    Y = Y1;
+    e = 0;
+    for (X = X1; X <= X2; X++) {
+        if (X > 0) {
+            if (swapcoords) {
+                if (X >= image->numRows) continue;
+                for (int y = Y - dW; y <= Y + dW; y++) {
+                    if (y < 0) continue;
+                    if (y >= image->numCols) continue;
+                    psVectorAppend(values, image->data.F32[X][y]);
+                }
+            } else {
+                if (X >= image->numCols) continue;
+                for (int y = Y - dW; y <= Y + dW; y++) {
+                    if (y < 0) continue;
+                    if (y >= image->numRows) continue;
+                    psVectorAppend(values, image->data.F32[y][X]);
+                }
+            }
+        }
+        e += dY;
+        e2 = 2 * e;
+        if (e2 > dX) {
+            Y++;
+            e -= dX;
+        }
+        if (e2 < -dX) {
+            Y--;
+            e += dX;
+        }
+    }
+    return values;
+}
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotRadiiFromProfiles.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRadiiFromProfiles.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRadiiFromProfiles.c	(revision 27840)
@@ -0,0 +1,160 @@
+# include "psphotInternal.h"
+
+// Given the Radial Profiles (radii, fluxes) determine the radius for each profile at the desired isophote
+
+bool psphotRadiiFromProfiles (pmSource *source, float fluxMin, float fluxMax) {
+
+    psAssert (source, "missing source");
+    psAssert (source->extpars, "missing extpars");
+    psAssert (source->extpars->radFlux, "missing radFlux");
+
+    pmSourceRadialFlux *profile = source->extpars->radFlux;
+
+    psFree(profile->isophotalRadii);
+    profile->isophotalRadii = psVectorAlloc(profile->theta->n, PS_TYPE_F32);
+
+    for (int i = 0; i < profile->theta->n; i++) {
+	psVector *radii = profile->radii->data[i];
+	psVector *fluxes = profile->fluxes->data[i];
+	float radius = psphotRadiusFromProfile (source, radii, fluxes, fluxMin, fluxMax);
+
+	// psphotPetrosianVisualProfileByAngle (radii, fluxes, radius);
+
+	// warn on NAN?
+	profile->isophotalRadii->data.F32[i] = radius;
+    }
+    return true;
+}
+
+float psphotRadiusFromProfile (pmSource *source, psVector *radius, psVector *flux, float fluxMin, float fluxMax) {
+
+    // 'flux' is a noisy sample of the galaxy radial profile at points 'radius'
+    // rebin flux into samples defined by the isophote Fo = 0.5*(fluxMax + fluxMin).  the noisy
+    // sample is cleaned by rebinning to a well-matched radial binning
+
+    // base selections on fluxes defined by the flux range dF
+    float fluxRange = fluxMax - fluxMin;
+
+    // examine data in the two ranges Fm - Fo and Fo - Fp to define the bin size
+    // XXX reconsider the fractional isophote value
+    float Fm = fluxMin + 0.10*fluxRange;
+    float Fo = fluxMin + 0.25*fluxRange;
+    float Fp = fluxMin + 0.50*fluxRange;
+    int Rbin = 1;
+      
+    // find the median radius of the points in the flux range Fm - Fp:
+    { 
+	// storage vector for stats
+	psVector *values = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+	psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+
+	for (int i = 0; i < flux->n; i++) {
+	    if (!isfinite(flux->data.F32[i])) continue;
+	    if (flux->data.F32[i] < Fm) continue;
+	    if (flux->data.F32[i] > Fp) continue;
+	    
+	    psVectorAppend (values, radius->data.F32[i]);
+	}
+	if (values->n > 1) {
+	    psVectorStats (stats, values, NULL, NULL, 0);
+
+	    // if we have a valid range, rebin with bin size 1/2 of median radius
+	    if (isfinite(stats->sampleMedian)) {
+		Rbin = MAX(1, 0.5*stats->sampleMedian);
+	    }
+	}
+	psFree (values);
+	psFree (stats);
+    }
+    Rbin = 3;
+
+    psVector *fluxBinned = NULL;
+    psVector *radiusBinned = NULL;
+
+    // do not bother rebinning if the bin size is only 2 or less
+    if (Rbin <= 2) {
+	fluxBinned = psMemIncrRefCounter (flux);
+	radiusBinned = psMemIncrRefCounter (radius);
+    } else {
+	// storage vector for stats
+	psVector *values = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+	psStats *stats = psStatsAlloc(PS_STAT_SAMPLE_MEDIAN);
+  
+	// rebinned vectors
+	fluxBinned = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+	radiusBinned = psVectorAllocEmpty (flux->n, PS_TYPE_F32);
+
+	// sort the flux by the radius
+	pmSourceRadialProfileSortPair (radius, flux);
+
+	int nOut = 0;
+	radiusBinned->data.F32[nOut] = (nOut + 0.5)*Rbin;
+	float Rmin = radiusBinned->data.F32[nOut] - 0.5*Rbin;
+	float Rmax = radiusBinned->data.F32[nOut] + 0.5*Rbin;
+
+	for (int i = 0; i < flux->n; i++) {
+	    if (radius->data.F32[i] < Rmin) {
+		// XXX not sure how we can hit this, if there is full coverage of radiusBinned
+		continue;
+	    }
+	    if (radius->data.F32[i] > Rmax) {
+		// calculate the value for the nOut bin
+		// XXX need to fix this as well psStats (stats, values);
+		float value;
+		if (values->n > 0) {
+		    psVectorStats (stats, values, NULL, NULL, 0);
+		    value = stats->sampleMedian;
+		} else {
+		    value = NAN;
+		}
+		fluxBinned->data.F32[nOut] = value;
+		nOut ++;
+		radiusBinned->data.F32[nOut] = (nOut + 0.5)*Rbin;
+		Rmin = radiusBinned->data.F32[nOut] - 0.5*Rbin;
+		Rmax = radiusBinned->data.F32[nOut] + 0.5*Rbin;
+		values->n = 0;
+		psStatsInit(stats);
+	    }
+	    if (!isfinite(flux->data.F32[i])) continue;
+	    psVectorAppend (values, flux->data.F32[i]);
+	}
+	fluxBinned->n = nOut;
+	radiusBinned->n = nOut;
+	psFree (values);
+	psFree(stats);
+    }
+
+    float Ro = NAN;
+    bool above = true;
+    for (int i = 0; i < fluxBinned->n; i++) {
+
+	if (!isfinite(fluxBinned->data.F32[i])) continue;
+
+	// find the largest radius that matches the flux transition
+	if (above && (fluxBinned->data.F32[i] < Fo)) {
+	    // XXX is there a macro in psLib that does this interpolation?
+	    if (i == 0) { 
+		psTrace ("psphot", 4, "bogus radial profile for source at %f, %f, skipping", source->peak->xf, source->peak->yf);
+		psFree (fluxBinned);
+		psFree (radiusBinned);
+		return NAN;
+	    } 
+	    Ro = radiusBinned->data.F32[i-1] + (radiusBinned->data.F32[i] - radiusBinned->data.F32[i-1]) * (Fo - fluxBinned->data.F32[i-1]) / (fluxBinned->data.F32[i] - fluxBinned->data.F32[i-1]);
+	    above = FALSE;
+	}
+  
+	if (!above && (fluxBinned->data.F32[i] >= Fo)) {
+	    above = TRUE;
+	}
+    }
+
+    // show the results
+    // psphotPetrosianVisualProfileRadii (radius, flux, radiusBinned, fluxBinned, fluxMax, Ro);
+
+    psFree(fluxBinned);
+    psFree(radiusBinned);
+    return Ro;
+}
+
+
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotRadiusChecks.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRadiusChecks.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRadiusChecks.c	(revision 27840)
@@ -4,14 +4,24 @@
 static float PSF_FIT_NSIGMA;
 static float PSF_FIT_PADDING;
+static float PSF_APERTURE = 0;	// radius to use in PSF aperture mags
 static float PSF_FIT_RADIUS = 0;	// radius to use in fitting (ignored if <= 0,
 					// and a per-object radius is calculated)
 
-bool psphotInitRadiusPSF(const psMetadata *recipe, const pmModelType type) {
+bool psphotInitRadiusPSF(const psMetadata *recipe, const psMetadata *analysis, const pmModelType type) {
 
     bool status = true;
 
-    PSF_FIT_NSIGMA  = psMetadataLookupF32(&status, recipe, "PSF_FIT_NSIGMA");
+    PSF_FIT_NSIGMA = psMetadataLookupF32(&status, recipe, "PSF_FIT_NSIGMA");
     PSF_FIT_PADDING = psMetadataLookupF32(&status, recipe, "PSF_FIT_PADDING");
-    PSF_FIT_RADIUS =  psMetadataLookupF32(&status, recipe, "PSF_FIT_RADIUS");
+
+    PSF_FIT_RADIUS =  psMetadataLookupF32(&status, analysis, "PSF_FIT_RADIUS");
+    if (!status) {
+	PSF_FIT_RADIUS = psMetadataLookupF32(&status, recipe, "PSF_FIT_RADIUS");
+    }
+
+    PSF_APERTURE =  psMetadataLookupF32(&status, analysis, "PSF_APERTURE");
+    if (!status) {
+	PSF_APERTURE =  psMetadataLookupF32(&status, recipe, "PSF_APERTURE");
+    }
 
     return true;
@@ -34,17 +44,21 @@
 	    radiusFit = model->modelRadius(model->params, 1.0);
 	}
+	model->fitRadius = (RADIUS_TYPE)(radiusFit + PSF_FIT_PADDING);
+    } else {
+	model->fitRadius = radiusFit;
     }
-    model->radiusFit = (RADIUS_TYPE)(radiusFit + PSF_FIT_PADDING);
-
-    if (isnan(model->radiusFit)) psAbort("error in radius");
+    if (isnan(model->fitRadius)) psAbort("error in radius");
 	
     if (source->mode & PM_SOURCE_MODE_SATSTAR) {
-	model->radiusFit *= 2;
+	model->fitRadius *= 2;
     }
 
-    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+    // radius used to measure aperture photometry
+    source->apRadius = PSF_APERTURE;
+
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius);
 
     // set the mask to flag the excluded pixels
-    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", markVal);
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius, "OR", markVal);
     return status;
 }
@@ -58,15 +72,31 @@
 
     // set the fit radius based on the object flux limit and the model
-    model->radiusFit = (RADIUS_TYPE) (model->modelRadius (model->params, PSF_FIT_NSIGMA*moments->dSky) + dR + PSF_FIT_PADDING);
-    if (isnan(model->radiusFit)) psAbort("error in radius");
-	
+    float radiusFit = PSF_FIT_RADIUS;
+    if (radiusFit <= 0) {		// use fixed radius
+	if (moments == NULL) {
+	    radiusFit = model->modelRadius(model->params, PSF_FIT_NSIGMA*moments->dSky);
+	} else {
+	    radiusFit = model->modelRadius(model->params, 1.0);
+	}
+	model->fitRadius = (RADIUS_TYPE)(radiusFit + PSF_FIT_PADDING);
+    } else {
+	model->fitRadius = radiusFit;
+    }
+    if (isnan(model->fitRadius)) psAbort("error in radius");
+
+    // above sets a radius for a single star, bump by blend separation
+    model->fitRadius += dR;
+
     if (source->mode &  PM_SOURCE_MODE_SATSTAR) {
-	model->radiusFit *= 2;
+	model->fitRadius *= 2;
     }
 
-    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+    // radius used to measure aperture photometry
+    source->apRadius = PSF_APERTURE;
+
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius);
 
     // set the mask to flag the excluded pixels
-    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", markVal);
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius, "OR", markVal);
     return status;
 }
@@ -74,4 +104,5 @@
 static float EXT_FIT_NSIGMA;
 static float EXT_FIT_PADDING;
+static float EXT_FIT_MAX_RADIUS;
 
 bool psphotInitRadiusEXT (psMetadata *recipe, pmModelType type) {
@@ -79,6 +110,7 @@
     bool status;
 
-    EXT_FIT_NSIGMA   = psMetadataLookupF32 (&status, recipe, "EXT_FIT_NSIGMA");
-    EXT_FIT_PADDING  = psMetadataLookupF32 (&status, recipe, "EXT_FIT_PADDING");
+    EXT_FIT_NSIGMA     = psMetadataLookupF32 (&status, recipe, "EXT_FIT_NSIGMA");
+    EXT_FIT_PADDING    = psMetadataLookupF32 (&status, recipe, "EXT_FIT_PADDING");
+    EXT_FIT_MAX_RADIUS = psMetadataLookupF32 (&status, recipe, "EXT_FIT_MAX_RADIUS");
 
     return true;
@@ -86,5 +118,52 @@
 
 // call this function whenever you (re)-define the EXT model
+float psphotSetRadiusEXT (pmReadout *readout, pmSource *source, psImageMaskType markVal) {
+
+    psAssert (source, "source not defined??");
+    psAssert (source->peak, "peak not defined??");
+
+    pmPeak *peak = source->peak;
+
+    // set the radius based on the footprint:
+    if (!peak->footprint) goto escape;
+    pmFootprint *footprint = peak->footprint;
+    if (!footprint->spans) goto escape;
+    if (footprint->spans->n < 1) goto escape;
+
+    // find the max radius
+    float radius = 0.0;
+    for (int j = 0; j < footprint->spans->n; j++) {
+	pmSpan *span = footprint->spans->data[j];
+
+	float dY  = span->y  - peak->yf;
+	float dX0 = span->x0 - peak->xf;
+	float dX1 = span->x1 - peak->xf;
+
+	radius = PS_MAX (radius, hypot(dY, dX0));
+	radius = PS_MAX (radius, hypot(dY, dX1));
+    }
+
+    radius += EXT_FIT_PADDING;
+    if (isnan(radius)) psAbort("error in radius");
+
+    radius = PS_MIN (radius, EXT_FIT_MAX_RADIUS);
+
+    // redefine the pixels if needed
+    pmSourceRedefinePixels (source, readout, peak->xf, peak->yf, radius);
+
+    // set the mask to flag the excluded pixels
+    psImageKeepCircle (source->maskObj, peak->xf, peak->yf, radius, "OR", markVal);
+    return radius;
+
+escape:
+    return NAN;
+    // bool result = psphotCheckRadiusEXT (readout, source, model, markVal);
+    // return result;
+}
+
+// alternative EXT radius based on model guess (for use without footprints)
 bool psphotCheckRadiusEXT (pmReadout *readout, pmSource *source, pmModel *model, psImageMaskType markVal) {
+
+    psAbort ("do not use this function");
 
     psF32 *PAR = model->params->data.F32;
@@ -96,12 +175,12 @@
     float rawRadius = model->modelRadius (model->params, EXT_FIT_NSIGMA*moments->dSky);
 
-    model->radiusFit = rawRadius + EXT_FIT_PADDING;
-    if (isnan(model->radiusFit)) psAbort("error in radius");
+    model->fitRadius = rawRadius + EXT_FIT_PADDING;
+    if (isnan(model->fitRadius)) psAbort("error in radius");
 
     // redefine the pixels if needed
-    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit);
+    bool status = pmSourceRedefinePixels (source, readout, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius);
 
     // set the mask to flag the excluded pixels
-    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->radiusFit, "OR", markVal);
+    psImageKeepCircle (source->maskObj, PAR[PM_PAR_XPOS], PAR[PM_PAR_YPOS], model->fitRadius, "OR", markVal);
     return status;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotReadout.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReadout.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReadout.c	(revision 27840)
@@ -3,5 +3,4 @@
 // this should be called by every program that links against libpsphot
 bool psphotInit (void) {
-
     psphotErrorRegister();              // register our error codes/messages
     psphotModelClassInit ();            // load implementation-specific models
@@ -12,9 +11,9 @@
 bool psphotReadout(pmConfig *config, const pmFPAview *view) {
 
-    // measure the total elapsed time in psphotReadout.  XXX the current threading plan
-    // for psphot envisions threading within psphotReadout, not multiple threads calling
-    // the same psphotReadout.  In the current plan, this dtime is the elapsed time used
-    // jointly by the multiple threads, not the total time used by all threads.
+    // measure the total elapsed time in psphotReadout.  dtime is the elapsed time used jointly
+    // by the multiple threads, not the total time used by all threads.
     psTimerStart ("psphotReadout");
+
+    pmModelClassSetLimits(PM_MODEL_LIMITS_LAX);
 
     // select the current recipe
@@ -24,126 +23,110 @@
         return false;
     }
+    // optional break-point for processing
+    char *breakPt = psMetadataLookupStr (NULL, recipe, "BREAK_POINT");
+    psAssert (breakPt, "configuration error: set BREAK_POINT");
 
     // set the photcode for this image
-    if (!psphotAddPhotcode (recipe, config, view, "PSPHOT.INPUT")) {
+    if (!psphotAddPhotcode (config, view)) {
         psError (PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
         return false;
     }
 
-    // find the currently selected readout
-    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
-    PS_ASSERT_PTR_NON_NULL (readout, false);
-
-    // optional break-point for processing
-    char *breakPt = psMetadataLookupStr (NULL, recipe, "BREAK_POINT");
-    PS_ASSERT_PTR_NON_NULL (breakPt, false);
-
     // Generate the mask and weight images, including the user-defined analysis region of interest
-    psphotSetMaskAndVariance (config, readout, recipe);
+    if (!psphotSetMaskAndVariance (config, view)) {
+        return psphotReadoutCleanup(config, view);
+    }
     if (!strcasecmp (breakPt, "NOTHING")) {
-        return psphotReadoutCleanup(config, readout, recipe, NULL, NULL, NULL);
-    }
-
-    // display the image, weight, mask (ch 1,2,3)
-    psphotVisualShowImage (readout);
+        return psphotReadoutCleanup(config, view);
+    }
 
     // generate a background model (median, smoothed image)
-    if (!psphotModelBackground (config, view, "PSPHOT.INPUT")) {
-        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL, NULL);
-    }
-    if (!psphotSubtractBackground (config, view, "PSPHOT.INPUT")) {
-        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL, NULL);
+    if (!psphotModelBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!psphotSubtractBackground (config, view)) {
+        return psphotReadoutCleanup (config, view);
     }
     if (!strcasecmp (breakPt, "BACKMDL")) {
-        return psphotReadoutCleanup (config, readout, recipe, NULL, NULL, NULL);
-    }
-
-    // display the backsub and backgnd images
-    psphotVisualShowBackground (config, view, readout);
-    
-    // run a single-model test if desired (exits from here if test is run)
-    psphotModelTest (config, view, recipe);
-
-    // load the psf model, if suppled.  FWHM_X,FWHM_Y,etc are saved in the recipe
-    pmPSF *psf = psphotLoadPSF (config, view, recipe);
-
-    // several functions below behave differently if we have a PSF model already
-    bool havePSF = (psf != NULL);
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // load the psf model, if suppled.  FWHM_X,FWHM_Y,etc are determined and saved on
+    // readout->analysis XXX this function currently only works with a single PSPHOT.INPUT
+    if (!psphotLoadPSF (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "error loading psf model");
+        return psphotReadoutCleanup (config, view);
+    }
 
     // find the detections (by peak and/or footprint) in the image.
-    pmDetections *detections = psphotFindDetections (NULL, readout, recipe);
-    if (!detections) {
-        psLogMsg ("psphot", 3, "unable to find detections in this image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, NULL);
-    }
-
-    // construct sources and measure basic stats
-    psArray *sources = psphotSourceStats (config, readout, detections);
-    if (!sources) return false;
+    if (!psphotFindDetections (config, view, true)) { // pass 1
+        // this only happens if we had an error in psphotFindDetections
+        psError (PSPHOT_ERR_UNKNOWN, false, "failure in peak analysis");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // construct sources and measure basic stats (saved on detections->newSources)
+    if (!psphotSourceStats (config, view, true)) { // pass 1
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+        return psphotReadoutCleanup (config, view);
+    }
     if (!strcasecmp (breakPt, "PEAKS")) {
-        return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
-    }
-
-    // find blended neighbors of very saturated stars
-    // XXX merge this with Basic Deblend?
-    psphotDeblendSatstars (readout, sources, recipe);
-
-    // mark blended peaks PS_SOURCE_BLEND
-    if (!psphotBasicDeblend (sources, recipe)) {
-        psLogMsg ("psphot", 3, "failed on deblend analysis");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
-    }
-
-    // classify sources based on moments, brightness
-    if (!psphotRoughClass (readout, sources, recipe, havePSF)) {
-        psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+        return psphotReadoutCleanup(config, view);
+    }
+
+    // find blended neighbors of very saturated stars (detections->newSources)
+    if (!psphotDeblendSatstars (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed on satstar deblend analysis");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // mark blended peaks PS_SOURCE_BLEND (detections->newSources)
+    if (!psphotBasicDeblend (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed on deblend analysis");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // classify sources based on moments, brightness.  if a PSF model has been loaded, the PSF
+    // clump defined for it is used not measured (detections->newSources)
+    if (!psphotRoughClass (config, view)) { // pass 1
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to determine rough classifications");
+        return psphotReadoutCleanup (config, view);
+    }
+    // if we were not supplied a PSF model, determine the IQ stats here (detections->newSources)
+    if (!psphotImageQuality (config, view)) { // pass 1
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to measure image quality");
+        return psphotReadoutCleanup(config, view);
     }
     if (!strcasecmp (breakPt, "MOMENTS")) {
-        return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
-    }
-
-    if (!havePSF && !psphotImageQuality (recipe, sources)) {
-        psLogMsg("psphot", 3, "failed to measure image quality");
-        return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
-    }
-
-    // if we were not supplied a PSF, choose one here
-    if (psf == NULL) {
-        // use bright stellar objects to measure PSF
-        // XXX if we do not have enough stars to generate the PSF, build one
-        // from the SEEING guess and model class
-        psf = psphotChoosePSF (readout, sources, recipe);
-        if (psf == NULL) {
-            psLogMsg ("psphot", 3, "failure to construct a psf model");
-            return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
-        }
-        havePSF = true;
+        return psphotReadoutCleanup(config, view);
+    }
+
+    // use bright stellar objects to measure PSF if we were supplied a PSF for any input file,
+    // this step is skipped
+    if (!psphotChoosePSF (config, view)) { // pass 1
+        psLogMsg ("psphot", 3, "failure to construct a psf model");
+        return psphotReadoutCleanup (config, view);
     }
     if (!strcasecmp (breakPt, "PSFMODEL")) {
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
-    }
-
-    psphotVisualShowPSFModel (readout, psf);
+        return psphotReadoutCleanup (config, view);
+    }
 
     // include externally-supplied sources
-    psphotLoadExtSources (config, view, sources);
-
-    // construct an initial model for each object
-    psphotGuessModels (config, readout, sources, psf);
-
-    // XXX test output of models
-    // psphotTestSourceOutput (readout, sources, recipe, psf);
-
-    // linear PSF fit to source peaks
-    psphotFitSourcesLinear (readout, sources, recipe, psf, FALSE);
-
-    // We have to place these visualizations here because the models are not realized until
-    // psphotGuessModels or fitted until psphotFitSourcesLinear.
-    psphotVisualShowPSFStars (recipe, psf, sources);
-    psphotVisualShowSatStars (recipe, psf, sources);
-
-    // identify CRs and extended sources
-    psphotSourceSize (config, readout, sources, recipe, 0);
+    // XXX fix this in the new multi-input context
+    // psphotLoadExtSources (config, view); // pass 1
+
+    // construct an initial model for each object, set the radius to fitRadius, set circular
+    // fit mask (detections->newSources)
+    psphotGuessModels (config, view); // pass 1
+
+    // merge the newly selected sources into the existing list
+    // NOTE: merge OLD and NEW
+    psphotMergeSources (config, view);
+
+    // linear PSF fit to source peaks, subtract the models from the image (in PSF mask)
+    psphotFitSourcesLinear (config, view, false); // pass 1 (detections->allSources)
+
+    // identify CRs and extended sources (only unmeasured sources are measured)
+    psphotSourceSize (config, view, true); // pass 1 (detections->allSources)
     if (!strcasecmp (breakPt, "ENSEMBLE")) {
         goto finish;
@@ -151,62 +134,65 @@
 
     // non-linear PSF and EXT fit to brighter sources
-    psphotBlendFit (config, readout, sources, psf);
+    // replace model flux, adjust mask as needed, fit, subtract the models (full stamp)
+    psphotBlendFit (config, view); // pass 1 (detections->allSources)
 
     // replace all sources
-    psphotReplaceAllSources (sources, recipe);
-
-    // linear fit to include all sources
-    psphotFitSourcesLinear (readout, sources, recipe, psf, TRUE);
+    psphotReplaceAllSources (config, view); // pass 1 (detections->allSources)
+
+    // linear fit to include all sources (subtract again)
+    // NOTE : apply to ALL sources (extended + psf)
+    psphotFitSourcesLinear (config, view, true); // pass 2 (detections->allSources)
 
     // if we only do one pass, skip to extended source analysis
-    if (!strcasecmp (breakPt, "PASS1")) {
-        goto pass1finish;
-    }
-
-    // XXX for the moment, drop the re-calc of the background (prove this works)
-    // replace background in residual image
-    // psphotSkyReplace (config, view);
-    // re-measure background model (median, smoothed image)
-    // psphotImageMedian (config, view);
+    if (!strcasecmp (breakPt, "PASS1")) goto pass1finish;
+
+    // NOTE: possibly re-measure background model here with objects subtracted / or masked
 
     // add noise for subtracted objects
-    psphotAddNoise (readout, sources, recipe);
-
-    // find fainter sources (pass 2)
-    detections = psphotFindDetections (detections, readout, recipe);
+    psphotAddNoise (config, view); // pass 1 (detections->allSources)
+
+    // find fainter sources
+    // NOTE: finds new peaks and new footprints, OLD and FULL set are saved on detections
+    psphotFindDetections (config, view, false); // pass 2 (detections->peaks, detections->footprints)
 
     // remove noise for subtracted objects (ie, return to normal noise level)
-    psphotSubNoise (readout, sources, recipe);
+    // NOTE: this needs to operate only on the OLD sources
+    psphotSubNoise (config, view); // pass 1 (detections->allSources)
 
     // define new sources based on only the new peaks
-    psArray *newSources = psphotSourceStats (config, readout, detections);
+    // NOTE: new sources are saved on detections->newSources
+    psphotSourceStats (config, view, false); // pass 2 (detections->newSources)
 
     // set source type
-    if (!psphotRoughClass (readout, newSources, recipe, havePSF)) {
+    // NOTE: apply only to detections->newSources
+    if (!psphotRoughClass (config, view)) { // pass 2 (detections->newSources)
         psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
-    }
-
-    // create full input models
-    psphotGuessModels (config, readout, newSources, psf);
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // create full input models, set the radius to fitRadius, set circular fit mask
+    // NOTE: apply only to detections->newSources
+    psphotGuessModels (config, view); // pass 2 (detections->newSources)
 
     // replace all sources so fit below applies to all at once
-    psphotReplaceAllSources (sources, recipe);
+    // NOTE: apply only to OLD sources (which have been subtracted)
+    psphotReplaceAllSources (config, view); // pass 2
 
     // merge the newly selected sources into the existing list
-    psphotMergeSources (sources, newSources);
-    psFree (newSources);
-
-    // linear fit to all sources
-    psphotFitSourcesLinear (readout, sources, recipe, psf, TRUE);
+    // NOTE: merge OLD and NEW
+    // XXX check on free of sources...
+    psphotMergeSources (config, view); // (detections->newSources + detections->allSources -> detections->allSources)
+
+    // NOTE: apply to ALL sources
+    psphotFitSourcesLinear (config, view, true); // pass 3 (detections->allSources)
 
 pass1finish:
 
     // measure source size for the remaining sources
-    psphotSourceSize (config, readout, sources, recipe, 0);
-
-    psphotExtendedSourceAnalysis (readout, sources, recipe);
-
-    psphotExtendedSourceFits (readout, sources, recipe);
+    // NOTE: applies only to NEW (unmeasured) sources
+    psphotSourceSize (config, view, false); // pass 2 (detections->allSources)
+
+    psphotExtendedSourceAnalysis (config, view); // pass 1 (detections->allSources)
+    psphotExtendedSourceFits (config, view); // pass 1 (detections->allSources)
 
 finish:
@@ -216,11 +202,16 @@
 
     // measure aperture photometry corrections
-    if (!psphotApResid (config, readout, sources, psf)) {
+    if (!psphotApResid (config, view)) { // pass 1 (detections->allSources)
         psLogMsg ("psphot", 3, "failed on psphotApResid");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+        return psphotReadoutCleanup (config, view);
     }
 
     // calculate source magnitudes
-    psphotMagnitudes(config, readout, view, sources, psf);
+    psphotMagnitudes(config, view); // pass 1 (detections->allSources)
+
+    if (!psphotEfficiency(config, view)) { // pass 1
+        psErrorStackPrint(stderr, "Unable to determine detection efficiencies from fake sources");
+        psErrorClear();
+    }
 
     // replace failed sources?
@@ -228,11 +219,10 @@
 
     // replace background in residual image
-    psphotSkyReplace (config, view);
+    psphotSkyReplace (config, view); // pass 1
 
     // drop the references to the image pixels held by each source
-    psphotSourceFreePixels (sources);
+    psphotSourceFreePixels (config, view); // pass 1
 
     // create the exported-metadata and free local data
-    return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
+    return psphotReadoutCleanup(config, view);
 }
-
Index: branches/simtest_nebulous_branches/psphot/src/psphotReadoutCleanup.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReadoutCleanup.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReadoutCleanup.c	(revision 27840)
@@ -1,8 +1,8 @@
 # include "psphotInternal.h"
 
-// psphotReadoutCleanup is called on exit from psphotReadout.  If the last raised error is
-// not a DATA error, then there was a serious problem.  Only in this case, or if the fail
-// on the stats measurement, do we return false
-bool psphotReadoutCleanup (pmConfig *config, pmReadout *readout, psMetadata *recipe, pmDetections *detections, pmPSF *psf, psArray *sources) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotReadoutCleanup (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
 
     // remove internal pmFPAfiles, if created
@@ -12,17 +12,52 @@
     }
     if (psErrorCodeLast() != PS_ERR_NONE) {
-        psFree (psf);
-        psFree (sources);
-        psFree (detections);
         return false;
     }
 
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotReadoutCleanupReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on psphotReadoutCleanup for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+
+    // XXX move this to top of loop?
+    pmKapaClose ();
+
+    return true;
+}
+
+// psphotReadoutCleanup is called on exit from psphotReadout.  If the last raised error is
+// not a DATA error, then there was a serious problem.  Only in this case, or if the fail
+// on the stats measurement, do we return false
+bool psphotReadoutCleanupReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
+
+    bool status = true;
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    // when psphotReadoutCleanup is called, these are not necessarily defined
+    pmPSF        *psf        = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psArray      *sources    = detections ? detections->allSources : NULL;
+    // XXX where do we free these, in here (psMetadataRemove?)
+
     // use the psf-model to measure FWHM stats
     if (psf) {
-        if (!psphotPSFstats (readout, recipe, psf)) {
+        if (!psphotPSFstats (readout, psf)) {
             psError(PSPHOT_ERR_PROG, false, "Failed to measure PSF shape parameters");
-            psFree (psf);
-            psFree (sources);
-            psFree (detections);
             return false;
         }
@@ -30,9 +65,6 @@
     // otherwise, use the source moments to measure FWHM stats
     if (!psf && sources) {
-        if (!psphotMomentsStats (readout, recipe, sources)) {
+        if (!psphotMomentsStats (readout, sources)) {
             psError(PSPHOT_ERR_PROG, false, "Failed to measure Moment shape parameters");
-            psFree (psf);
-            psFree (sources);
-            psFree (detections);
             return false;
         }
@@ -40,29 +72,27 @@
 
     // Check to see if the image quality was measured
-    if (!psf) {
+    // XXX not sure we want / need this test
+    if (0 && !psf) {
         bool mdok;                      // Status of MD lookup
         int nIQ = psMetadataLookupS32(&mdok, recipe, "IQ_NSTAR"); // Number of stars for IQ measurement
         if (!mdok || nIQ <= 0) {
             psError(PSPHOT_ERR_DATA, false, "Unable to measure image quality");
-            psFree (psf);
-            psFree (sources);
-            psFree (detections);
             return false;
         }
     }
 
+    // create an output header with stats results currently saved on readout->analysis
+    psMetadata *header = psphotDefineHeader (readout->analysis);
 
     // write NSTARS to the image header
-    psphotSetHeaderNstars (recipe, sources);
-
-    // create an output header with stats results
-    psMetadata *header = psphotDefineHeader (recipe);
+    psphotSetHeaderNstars (header, sources);
 
     // save the results of the analysis
+    // this should happen way up stream (when needed?)
     psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.HEADER",  PS_DATA_METADATA, "header stats", header);
-    if (sources) {
-        psMetadataAdd (readout->analysis, PS_LIST_TAIL, "PSPHOT.SOURCES", PS_DATA_ARRAY, "psphot sources", sources);
-    }
+
     if (psf) {
+	// XXX this seems a little silly : we saved the psf on readout->analysis above, but now
+	// we are moving it to chip->analysis.
         // save the psf for possible output.  if there was already an entry, it was loaded from external sources
         // the new one may have been updated or modified, so replace the existing entry.  We
@@ -79,12 +109,5 @@
     }
 
-    // XXX move this to top of loop?
-    pmKapaClose ();
-
-    psFree (detections);
-    psFree (psf);
     psFree (header);
-    psFree (sources);
-
     return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotReadoutFindPSF.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReadoutFindPSF.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReadoutFindPSF.c	(revision 27840)
@@ -7,26 +7,12 @@
     psTimerStart ("psphotReadout");
 
-    // select the current recipe
-    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
-    if (!recipe) {
-        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
-        return false;
-    }
-
-    // set the photcode for this image
-    if (!psphotAddPhotcode(recipe, config, view, "PSPHOT.INPUT")) {
+    // set the photcode for the PSPHOT.INPUT
+    if (!psphotAddPhotcode(config, view)) {
         psError(PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
         return false;
     }
 
-    // find the currently selected readout
-    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
-    PS_ASSERT_PTR_NON_NULL (readout, false);
-
     // Generate the mask and variance images, including the user-defined analysis region of interest
-    psphotSetMaskAndVariance (config, readout, recipe);
-
-    // display the image, weight, mask (ch 1,2,3)
-    psphotVisualShowImage (readout);
+    psphotSetMaskAndVariance (config, view);
 
     // Note that in this implementation, we do NOT model the background and we do not
@@ -34,40 +20,62 @@
 
     // include externally-supplied sources (supplied as PSPHOT.INPUT.CMF)
-    pmDetections *detections = psphotDetectionsFromSources (config, inSources);
-    if (!detections || !detections->peaks) {
+    // XXX we assume a single set of input sources is supplied
+    if (!psphotDetectionsFromSources (config, view, inSources)) {
         psError(PSPHOT_ERR_ARGUMENTS, true, "Can't find PSF stars");
-        return psphotReadoutCleanup(config, readout, recipe, detections, NULL, NULL);
+        return psphotReadoutCleanup(config, view);
     }
 
-    // construct sources and measure basic stats (moments, local sky)
-    psArray *sources = psphotSourceStats(config, readout, detections);
-    if (!sources) return false;
+    // construct detections->newSources and measure basic stats (moments, local sky)
+    if (!psphotSourceStats(config, view, true)) {
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+	return false;
+    }
 
-    // peak flux is wrong : set based on previous image
-    // use the peak measured in the moments analysis:
-    for (int i = 0; i < sources->n; i++) {
-      pmSource *source = sources->data[i];
-      source->peak->flux = source->moments->Peak;
+    // peak flux is wrong : use the peak measured in the moments analysis:
+    if (!psphotRepairLoadedSources(config, view)) {
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to repair sources");
+	return false;
     }
 
     // classify sources based on moments, brightness (psf is not known)
-    if (!psphotRoughClass (readout, sources, recipe, false)) {
-        psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, NULL, sources);
+    if (!psphotRoughClass (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to determine rough source class");
+        return psphotReadoutCleanup (config, view);
     }
 
-    pmPSF *psf = psphotChoosePSF(readout, sources, recipe);
-    if (!psf) {
+    if (!psphotImageQuality (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to measure image quality");
+        return psphotReadoutCleanup(config, view);
+    }
+
+    if (!psphotChoosePSF(config, view)) {
         psError(PSPHOT_ERR_PSF, false, "Failed to construct a psf model");
-        psFree(sources);
-        return psphotReadoutCleanup(config, readout, recipe, detections, NULL, NULL);
+        return psphotReadoutCleanup(config, view);
     }
-    psphotVisualShowPSFModel(readout, psf);
+
+# if 0
+    // XXX if we want to determine the aperture residual correction here, we either
+    // need to carry it out of the PSF determination analysis above, or save the model
+    // fits from that analysis, or run the linear PSF fit for all objects currently in hand
+    // construct an initial model for each object, set the radius to fitRadius, set circular fit mask
+    psphotGuessModels (config, view);
+# endif
+
+    // merge the newly selected sources into the existing list
+    // NOTE: merge OLD and NEW
+    psphotMergeSources (config, view); 
+
+# if 0
+    // measure aperture photometry corrections
+    if (!psphotApResid (config, view)) {
+        psLogMsg ("psphot", 3, "failed on psphotApResid");
+        return psphotReadoutCleanup (config, view);
+    }
+# endif
 
     // drop the references to the image pixels held by each source
-    psphotSourceFreePixels(sources);
-    psFree(sources);
+    psphotSourceFreePixels(config, view);
 
     // create the exported-metadata and free local data
-    return psphotReadoutCleanup(config, readout, recipe, detections, psf, NULL);
+    return psphotReadoutCleanup(config, view);
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotReadoutKnownSources.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReadoutKnownSources.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReadoutKnownSources.c	(revision 27840)
@@ -1,32 +1,18 @@
 # include "psphotInternal.h"
 
-// in this psphotReadout-variant, we are only measuring the photometry for known sources,
-// using a PSF generated from this observation those sources
+// in this psphotReadout-variant, we are only measuring the photometry for known sources, using
+// a PSF generated for this observation from those sources
 bool psphotReadoutKnownSources(pmConfig *config, const pmFPAview *view, psArray *inSources) {
 
     psTimerStart ("psphotReadout");
 
-    // select the current recipe
-    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
-    if (!recipe) {
-        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
-        return false;
-    }
-
     // set the photcode for this image
-    if (!psphotAddPhotcode(recipe, config, view, "PSPHOT.INPUT")) {
+    if (!psphotAddPhotcode(config, view)) {
         psError(PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
         return false;
     }
 
-    // find the currently selected readout
-    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
-    PS_ASSERT_PTR_NON_NULL (readout, false);
-
     // Generate the mask and weight images, including the user-defined analysis region of interest
-    psphotSetMaskAndVariance (config, readout, recipe);
-
-    // display the image, weight, mask (ch 1,2,3)
-    psphotVisualShowImage (readout);
+    psphotSetMaskAndVariance (config, view);
 
     // Note that in this implementation, we do NOT model the background and we do not
@@ -34,58 +20,55 @@
 
     // include externally-supplied sources (supplied as PSPHOT.INPUT.CMF)
-    pmDetections *detections = psphotDetectionsFromSources (config, inSources);
-    if (!detections || !detections->peaks) {
+    if (!psphotDetectionsFromSources (config, view, inSources)) {
         psError(PSPHOT_ERR_ARGUMENTS, true, "Can't find PSF stars");
-        return psphotReadoutCleanup(config, readout, recipe, detections, NULL, NULL);
+        return psphotReadoutCleanup(config, view);
     }
 
     // construct sources and measure basic stats
-    psArray *sources = psphotSourceStats (config, readout, detections);
-    if (!sources) return false;
+    if (!psphotSourceStats (config, view, true)) {
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+	return false;
+    }
 
-    // peak flux is wrong : set based on previous image
-    // use the peak measured in the moments analysis:
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-        source->peak->flux = source->moments->Peak;
+    // peak flux is wrong : use the peak measured in the moments analysis:
+    if (!psphotRepairLoadedSources(config, view)) {
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to repair sources");
+	return false;
     }
 
     // classify sources based on moments, brightness (psf is not known)
-    if (!psphotRoughClass (readout, sources, recipe, false)) {
-        psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, NULL, sources);
+    if (!psphotRoughClass (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to determine rough source class");
+        return psphotReadoutCleanup(config, view);
     }
 
-    pmPSF *psf = psphotChoosePSF (readout, sources, recipe);
-    if (!psf) {
-        psLogMsg ("psphot", 3, "failure to construct a psf model");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+    if (!psphotChoosePSF (config, view)) {
+        psError(PSPHOT_ERR_PSF, false, "Failed to construct a psf model");
+        return psphotReadoutCleanup(config, view);
     }
-    psphotVisualShowPSFModel (readout, psf);
 
     // construct an initial model for each object
-    psphotGuessModels (config, readout, sources, psf);
+    psphotGuessModels (config, view);
+
+    // merge the newly selected sources into the existing list
+    // NOTE: merge OLD and NEW
+    psphotMergeSources (config, view); 
 
     // linear PSF fit to source peaks
-    psphotFitSourcesLinear (readout, sources, recipe, psf, FALSE);
-
-    // We have to place these visualizations here because the models are not realized until
-    // psphotGuessModels or fitted until psphotFitSourcesLinear.
-    psphotVisualShowPSFStars (recipe, psf, sources);
-    psphotVisualShowSatStars (recipe, psf, sources);
+    psphotFitSourcesLinear (config, view, false);
 
     // measure aperture photometry corrections
-    if (!psphotApResid (config, readout, sources, psf)) {
+    if (!psphotApResid (config, view)) {
         psLogMsg ("psphot", 3, "failed on psphotApResid");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+        return psphotReadoutCleanup(config, view);
     }
 
     // calculate source magnitudes
-    psphotMagnitudes(config, readout, view, sources, psf);
+    psphotMagnitudes(config, view);
 
     // drop the references to the image pixels held by each source
-    psphotSourceFreePixels (sources);
+    psphotSourceFreePixels (config, view);
 
     // create the exported-metadata and free local data
-    return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
+    return psphotReadoutCleanup(config, view);
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotReadoutMinimal.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReadoutMinimal.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReadoutMinimal.c	(revision 27840)
@@ -1,7 +1,7 @@
 # include "psphotInternal.h"
 
-// this badly-named function performs photometry assuming (a) a supplied PSF, (b)
-// background subtraction, (c) linear psf-model fits only.  it is currently only being
-// used by ppSub.
+// this badly-named function performs photometry assuming (a) a supplied PSF, (b) background
+// subtraction, (c) linear psf-model fits only, (d) a prior analysis has supplied the moments
+// window parameters.  It is currently only being used by ppSub.
 
 // NOTE: ppSub needs to perform extended source analysis for comets and trails.
@@ -15,90 +15,80 @@
     psTimerStart ("psphotReadout");
 
-    // select the current recipe
-    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
-    if (!recipe) {
-        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
-        return false;
-    }
+    pmModelClassSetLimits(PM_MODEL_LIMITS_LAX);
 
     // set the photcode for this image
-    if (!psphotAddPhotcode(recipe, config, view, "PSPHOT.INPUT")) {
+    if (!psphotAddPhotcode(config, view)) {
         psError(PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
         return false;
     }
 
-    // find the currently selected readout
-    pmReadout  *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
-    PS_ASSERT_PTR_NON_NULL (readout, false);
+    // Generate the mask and weight images, including the user-defined analysis region of interest
+    psphotSetMaskAndVariance (config, view);
 
-    // Generate the mask and weight images, including the user-defined analysis region of interest
-    psphotSetMaskAndVariance (config, readout, recipe);
-
-    // display the image, weight, mask (ch 1,2,3)
-    psphotVisualShowImage (readout);
-
-    // load the psf model, if suppled.  FWHM_X,FWHM_Y,etc are saved in the recipe
-    pmPSF *psf = psphotLoadPSF (config, view, recipe);
-    if (!psf) {
+    // load the psf model, if suppled.  FWHM_X,FWHM_Y,etc are saved on readout->analysis
+    if (!psphotLoadPSF (config, view)) {
       psError (PSPHOT_ERR_CONFIG, false, "missing psf model");
-      return psphotReadoutCleanup (config, readout, recipe, NULL, NULL, NULL);
+      return psphotReadoutCleanup (config, view);
     }
 
-    // find the detections (by peak and/or footprint) in the image.
-    pmDetections *detections = pmDetectionsAlloc(); // New detections; allocated to ensure pass=2
-    detections = psphotFindDetections(detections, readout, recipe);
-    if (!detections) {
-        psLogMsg ("psphot", 3, "unable to find detections in this image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, NULL);
+    // find the detections (by peak and/or footprint) in the image. (final pass)
+    if (!psphotFindDetections(config, view, false)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failure in peak analysis");
+        return psphotReadoutCleanup (config, view);
     }
 
-    // construct sources and measure basic stats
-    psArray *sources = psphotSourceStats (config, readout, detections);
-    if (!sources) return false;
+    // construct sources and measure basic stats (saved on detections->newSources)
+    if (!psphotSourceStats (config, view, false)) { // pass 1
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+        return psphotReadoutCleanup (config, view);
+    }
 
     // find blended neighbors of very saturated stars
-    // XXX merge this with Basic Deblend?
-    psphotDeblendSatstars (readout, sources, recipe);
+    psphotDeblendSatstars (config, view);
 
     // mark blended peaks PS_SOURCE_BLEND
-    if (!psphotBasicDeblend (sources, recipe)) {
+    if (!psphotBasicDeblend (config, view)) {
         psLogMsg ("psphot", 3, "failed on deblend analysis");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+        return psphotReadoutCleanup (config, view);
     }
 
     // classify sources based on moments, brightness (use supplied psf shape parameters)
-    if (!psphotRoughClass (readout, sources, recipe, true)) {
+    if (!psphotRoughClass (config, view)) {
         psLogMsg ("psphot", 3, "failed to find a valid PSF clump for image");
-        return psphotReadoutCleanup (config, readout, recipe, detections, psf, sources);
+        return psphotReadoutCleanup (config, view);
     }
 
     // construct an initial model for each object
-    psphotGuessModels (config, readout, sources, psf);
+    psphotGuessModels (config, view);
+
+    // merge the newly selected sources into the existing list
+    psphotMergeSources (config, view);
 
     // linear PSF fit to source peaks
-    psphotFitSourcesLinear (readout, sources, recipe, psf, FALSE);
-
-    // We have to place these visualizations here because the models are not realized until
-    // psphotGuessModels or fitted until psphotFitSourcesLinear.
-    psphotVisualShowPSFStars (recipe, psf, sources);
-    psphotVisualShowSatStars (recipe, psf, sources);
+    psphotFitSourcesLinear (config, view, false);
 
 // XXX eventually, add the extended source fits here
 # if (0)
     // measure source size for the remaining sources
-    psphotSourceSize (config, readout, sources, recipe, 0);
+    psphotSourceSize (config, view);
 
-    psphotExtendedSourceAnalysis (readout, sources, recipe);
+    psphotExtendedSourceAnalysis (config, view);
 
-    psphotExtendedSourceFits (readout, sources, recipe);
+    psphotExtendedSourceFits (config, view);
 # endif
 
     // calculate source magnitudes
-    psphotMagnitudes(config, readout, view, sources, psf);
+    psphotMagnitudes(config, view);
+
+    // XXX ensure this is measured if the analysis succeeds (even if quality is low)
+    if (!psphotEfficiency(config, view)) {
+        psErrorStackPrint(stderr, "Unable to determine detection efficiencies from fake sources");
+        psErrorClear();
+    }
 
     // drop the references to the image pixels held by each source
-    psphotSourceFreePixels (sources);
+    psphotSourceFreePixels (config, view);
 
     // create the exported-metadata and free local data
-    return psphotReadoutCleanup(config, readout, recipe, detections, psf, sources);
+    return psphotReadoutCleanup(config, view);
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotReplaceUnfit.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotReplaceUnfit.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotReplaceUnfit.c	(revision 27840)
@@ -17,5 +17,4 @@
     replace:
         pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-        source->tmpFlags &= ~PM_SOURCE_TMPF_SUBTRACTED;
     }
     psLogMsg ("psphot.replace", 3, "replace unfitted models: %f sec (%ld objects)\n", psTimerMark ("psphot.replace"), sources->n);
@@ -23,5 +22,27 @@
 }
 
-bool psphotReplaceAllSources (psArray *sources, psMetadata *recipe) {
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotReplaceAllSources (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotReplaceAllSourcesReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to replace all sources for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotReplaceAllSourcesReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
@@ -30,7 +51,20 @@
     psTimerStart ("psphot.replace");
 
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
     psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
-    assert (maskVal);
+    psAssert (maskVal, "missing mask value?");
 
     for (int i = 0; i < sources->n; i++) {
@@ -41,5 +75,4 @@
 
       pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-      source->tmpFlags &= ~PM_SOURCE_TMPF_SUBTRACTED;
     }
     psLogMsg ("psphot.replace", PS_LOG_INFO, "replaced models for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot.replace"));
@@ -47,5 +80,5 @@
 }
 
-bool psphotRemoveAllSources (psArray *sources, psMetadata *recipe) {
+bool psphotRemoveAllSources (const psArray *sources, const psMetadata *recipe) {
 
     bool status;
@@ -64,46 +97,7 @@
       if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) continue;
 
-      pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-      source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
+      pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
     }
     psLogMsg ("psphot.replace", PS_LOG_INFO, "replaced models for %ld objects: %f sec\n", sources->n, psTimerMark ("psphot.replace"));
     return true;
 }
-
-// add source, if the source has been subtracted; do not modify state
-bool psphotAddWithTest (pmSource *source, bool useState, psImageMaskType maskVal) {
-
-    // what is current state? (true : add; false : sub)
-    bool state = !(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
-    if (state && useState) return true;
-
-    pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-    return true;
-}
-
-// sub source, if the source has been added; do not modify state
-bool psphotSubWithTest (pmSource *source, bool useState, psImageMaskType maskVal) {
-
-    // what is current state? (true : sub; false : add)
-    bool state = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
-    if (state && useState) return true;
-
-    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-    return true;
-}
-
-// add or sub source to match recorded state: supply current state as true (add) or false (sub)
-bool psphotSetState (pmSource *source, bool curState, psImageMaskType maskVal) {
-
-    // what is desired state? (true : add; false : sub)
-    bool newState = !(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
-    if (curState == newState) return true;
-
-    if (curState && !newState) {
-        pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-    }
-    if (newState && !curState) {
-        pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-    }
-    return true;
-}
Index: branches/simtest_nebulous_branches/psphot/src/psphotRoughClass.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotRoughClass.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotRoughClass.c	(revision 27840)
@@ -1,18 +1,66 @@
 # include "psphotInternal.h"
 
-# define CHECK_STATUS(S,MSG) {						\
-	if (!status) {							\
-	    psError(PSPHOT_ERR_CONFIG, false, "missing PSF Clump entry: %s\n", MSG); \
-	    return false;						\
-	} }
+# define CHECK_STATUS(S,MSG) {                                          \
+        if (!status) {                                                  \
+            psError(PSPHOT_ERR_CONFIG, false, "missing PSF Clump entry: %s\n", MSG); \
+            return false;                                               \
+        } }
 
-bool psphotRoughClassRegion (int nRegion, psRegion *region, psArray *sources, psMetadata *recipe, const bool havePSF);
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotRoughClass (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
 
-// 2006.02.02 : no leaks
-bool psphotRoughClass (pmReadout *readout, psArray *sources, psMetadata *recipe, const bool havePSF) {
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+	if (!psphotRoughClassReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on rough classification for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotRoughClassReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe) {
 
     bool status;
 
     psTimerStart ("psphot.rough");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    // if we have a PSF, use the existing PSF clump region below
+    bool havePSF = false;
+    if (psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF")) {
+	havePSF = true;
+    }
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->newSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+	psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping rough classification");
+	return true;
+    }
 
     // we make this measurement on a NxM grid of regions across the readout
@@ -24,17 +72,18 @@
     int nRegion = 0;
     for (int ix = 0; ix < NX; ix ++) {
-	for (int iy = 0; iy < NY; iy ++) {
+        for (int iy = 0; iy < NY; iy ++) {
 
-	    psRegion region = psRegionSet (ix*dX, (ix + 1)*dX, iy*dY, (iy + 1)*dY);
-	    if (!psphotRoughClassRegion (nRegion, &region, sources, recipe, havePSF)) {
-		psLogMsg ("psphot", 4, "Failed to determine rough classification for region %f,%f - %f,%f\n", 
-			 region.x0, region.y0, region.x1, region.y1);
-		continue;
-	    }
-	    
-	    nRegion ++;
-	}
+            psRegion *region = psRegionAlloc (ix*dX, (ix + 1)*dX, iy*dY, (iy + 1)*dY);
+            if (!psphotRoughClassRegion (nRegion, region, sources, readout->analysis, recipe, havePSF)) {
+                psLogMsg ("psphot", 4, "Failed to determine rough classification for region %f,%f - %f,%f\n",
+                         region->x0, region->y0, region->x1, region->y1);
+                psFree (region);
+                continue;
+            }
+            psFree (region);
+            nRegion ++;
+        }
     }
-    psMetadataAddS32 (recipe, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", nRegion);
+    psMetadataAddS32 (readout->analysis, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", nRegion);
 
     // optional printout of source moments only
@@ -43,12 +92,12 @@
     psLogMsg ("psphot.roughclass", PS_LOG_INFO, "rough classification: %f sec\n", psTimerMark ("psphot.rough"));
 
-    psphotVisualPlotMoments (recipe, sources);
+    psphotVisualPlotMoments (recipe, readout->analysis, sources);
     psphotVisualShowRoughClass (sources);
-    psphotVisualShowFlags (sources);
+    // XXX better visualization: psphotVisualShowFlags (sources);
 
     return true;
 }
 
-bool psphotRoughClassRegion (int nRegion, psRegion *region, psArray *sources, psMetadata *recipe, const bool havePSF) {
+bool psphotRoughClassRegion (int nRegion, psRegion *region, psArray *sources, psMetadata *analysis, psMetadata *recipe, const bool havePSF) {
 
     bool status;
@@ -60,46 +109,75 @@
     assert (maskSat);
 
+    // the regions are saved on the readout->analysis metadata folder (passed to us as analysis)
     snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", nRegion);
-    psMetadata *regionMD = psMetadataLookupPtr (&status, recipe, regionName);
+    psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
     if (!regionMD) {
-	regionMD = psMetadataAlloc();
-	psMetadataAddMetadata (recipe, PS_LIST_TAIL, regionName, PS_META_REPLACE, "psf clump region", regionMD);
-	psFree (regionMD);
+        // allocate the region metadata folder and add this region to it.
+        regionMD = psMetadataAlloc();
+        psMetadataAddMetadata (analysis, PS_LIST_TAIL, regionName, PS_META_REPLACE, "psf clump region", regionMD);
+        psFree (regionMD);
     }
+    psMetadataAddPtr (regionMD, PS_LIST_TAIL, "REGION", PS_DATA_REGION | PS_META_REPLACE, "psf clump region", region);
 
     if (!havePSF) {
-	// determine the PSF parameters from the source moment values
-	psfClump = pmSourcePSFClump (region, sources, recipe);
-	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
-	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
-	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
-	psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump center", psfClump.dY);
+        // determine the PSF parameters from the source moment values
+        // XXX why not save the psfClump as a PTR?
+
+	float PSF_SN_LIM = psMetadataLookupF32(&status, recipe, "PSF_SN_LIM"); psAssert (status, "missing PSF_SN_LIM");
+	float MOMENTS_AR_MAX = psMetadataLookupF32(&status, recipe, "MOMENTS_AR_MAX"); psAssert (status, "missing MOMENTS_AR_MAX");
+
+	float PSF_CLUMP_GRID_SCALE = psMetadataLookupF32(&status, analysis, "PSF_CLUMP_GRID_SCALE");
+	if (!status) {
+	    PSF_CLUMP_GRID_SCALE = psMetadataLookupF32(&status, recipe, "PSF_CLUMP_GRID_SCALE");
+	    psAssert (status, "missing PSF_CLUMP_GRID_SCALE");
+	}
+	float MOMENTS_SX_MAX = psMetadataLookupF32(&status, analysis, "MOMENTS_SX_MAX");
+	if (!status) {
+	    MOMENTS_SX_MAX = psMetadataLookupF32(&status, recipe, "MOMENTS_SX_MAX");
+	    psAssert (status, "missing MOMENTS_SX_MAX");
+	}
+	float MOMENTS_SY_MAX = psMetadataLookupF32(&status, analysis, "MOMENTS_SY_MAX");
+	if (!status) {
+	    MOMENTS_SY_MAX = psMetadataLookupF32(&status, recipe, "MOMENTS_SY_MAX");
+	    psAssert (status, "missing MOMENTS_SY_MAX");
+	}
+
+        psfClump = pmSourcePSFClump (NULL, region, sources, PSF_SN_LIM, PSF_CLUMP_GRID_SCALE, MOMENTS_SX_MAX, MOMENTS_SY_MAX, MOMENTS_AR_MAX);
+
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump center", psfClump.dY);
     } else {
-	// pull FWHM_X,Y from the recipe, use to define psfClump.X,Y
-	psfClump.X  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X"); 
-	if (!status) {
-	    psLogMsg ("psphot", 4, "No PSF clump defined for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
-	    return false;
-	}	    
-	psfClump.Y  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");   psAssert (status, "missing PSF.CLUMP.Y");
-	psfClump.dX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");  psAssert (status, "missing PSF.CLUMP.DX");
-	psfClump.dY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");  psAssert (status, "missing PSF.CLUMP.DY");
+        // pull FWHM_X,Y from the recipe, use to define psfClump.X,Y
+        psfClump.X  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
+        if (!status) {
+            psLogMsg ("psphot", 4, "No PSF clump defined for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
+            return false;
+        }
+        psfClump.Y  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");   psAssert (status, "missing PSF.CLUMP.Y");
+        psfClump.dX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");  psAssert (status, "missing PSF.CLUMP.DX");
+        psfClump.dY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");  psAssert (status, "missing PSF.CLUMP.DY");
     }
 
     if (psfClump.X < 0) {
-	psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourcePSFClump");
-	return false;
+        psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourcePSFClump");
+        return false;
     }
     if (!psfClump.X || !psfClump.Y || isnan(psfClump.X) || isnan(psfClump.Y)) {
-	psLogMsg ("psphot", 4, "Failed to find a valid PSF clump for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
-	return false;
+        psLogMsg ("psphot", 4, "Failed to find a valid PSF clump for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
+        return false;
     }
     psLogMsg ("psphot", 3, "psf clump  X,  Y: %f, %f\n", psfClump.X, psfClump.Y);
     psLogMsg ("psphot", 3, "psf clump DX, DY: %f, %f\n", psfClump.dX, psfClump.dY);
 
+    // get basic parameters, or set defaults
+    float PSF_SN_LIM = psMetadataLookupF32 (&status, recipe, "PSF_SN_LIM"); psAssert (status, "missing PSF_SN_LIM");
+    float PSF_CLUMP_NSIGMA = psMetadataLookupF32 (&status, recipe, "PSF_CLUMP_NSIGMA"); psAssert (status, "missing PSF_CLUMP_NSIGMA");
+
     // group into STAR, COSMIC, EXTENDED, SATURATED, etc.
-    if (!pmSourceRoughClass (region, sources, recipe, psfClump, maskSat)) {
-	psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourceRoughClass");
-	return false;
+    if (!pmSourceRoughClass (region, sources, PSF_SN_LIM, PSF_CLUMP_NSIGMA, psfClump, maskSat)) {
+        psError(PSPHOT_ERR_PROG, false, "programming error calling pmSourceRoughClass");
+        return false;
     }
 
Index: branches/simtest_nebulous_branches/psphot/src/psphotSetThreads.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSetThreads.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSetThreads.c	(revision 27840)
@@ -10,5 +10,5 @@
     psFree(task);
 
-    task = psThreadTaskAlloc("PSPHOT_MAGNITUDES", 8);
+    task = psThreadTaskAlloc("PSPHOT_MAGNITUDES", 9);
     task->function = &psphotMagnitudes_Threaded;
     psThreadTaskAdd(task);
@@ -20,10 +20,10 @@
     psFree(task);
 
-    task = psThreadTaskAlloc("PSPHOT_APRESID_MAGS", 6);
+    task = psThreadTaskAlloc("PSPHOT_APRESID_MAGS", 7);
     task->function = &psphotApResidMags_Threaded;
     psThreadTaskAdd(task);
     psFree(task);
 
-    task = psThreadTaskAlloc("PSPHOT_SOURCE_STATS", 5);
+    task = psThreadTaskAlloc("PSPHOT_SOURCE_STATS", 10);
     task->function = &psphotSourceStats_Threaded;
     psThreadTaskAdd(task);
Index: branches/simtest_nebulous_branches/psphot/src/psphotSignificanceImage.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSignificanceImage.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSignificanceImage.c	(revision 27840)
@@ -22,24 +22,27 @@
     }
 
-    bool status_x, status_y;
-    float FWHM_X = psMetadataLookupF32 (&status_x, recipe, "FWHM_X");
-    float FWHM_Y = psMetadataLookupF32 (&status_y, recipe, "FWHM_Y");
-    if (status_x && status_y) {
-      // if we know the FHWM, use that to set the smoothing kernel (XXX allow an optional override?)
-      SIGMA_SMTH  = 0.5*(FWHM_X + FWHM_Y) / (2.0*sqrt(2.0*log(2.0)));
-      NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
-      guess = false;
+    // if we have already determined the PSF model, then we have a better idea how to smooth this image
+    bool statusMajor, statusMinor;
+    float fwhmMajor = psMetadataLookupF32(&statusMajor, readout->analysis, "FWHM_MAJ");
+    float fwhmMinor = psMetadataLookupF32(&statusMinor, readout->analysis, "FWHM_MIN");
+    if (statusMajor && statusMinor) {
+        // if we know the FHWM, use that to set the smoothing kernel (XXX allow an optional override?)
+        if (!isfinite(fwhmMajor) || !isfinite(fwhmMinor) || fwhmMajor == 0.0 || fwhmMinor == 0.0) {
+            psWarning("fwhmMajor (%f) or fwhmMinor (%f) is bad!", fwhmMajor, fwhmMinor);
+        }
+        SIGMA_SMTH  = 0.5*(fwhmMajor + fwhmMinor) / (2.0*sqrt(2.0*log(2.0)));
+        NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
+        guess = false;
     } else {
-      // if we do not know the FWHM, use the guess smoothing kernel supplied.
-      // it is a configuration error if these are not supplied
-      SIGMA_SMTH  = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_SIGMA");
-      PS_ASSERT (status, NULL);
-      NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
-      PS_ASSERT (status, NULL);
-      guess = true;
+        // if we do not know the FWHM, use the guess smoothing kernel supplied.
+        // it is a configuration error if these are not supplied
+        SIGMA_SMTH  = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_SIGMA");
+        PS_ASSERT (status, NULL);
+        NSIGMA_SMTH = psMetadataLookupF32 (&status, recipe, "PEAKS_SMOOTH_NSIGMA");
+        PS_ASSERT (status, NULL);
+        guess = true;
     }
     // record the actual smoothing sigma
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "SIGMA_SMOOTH", PS_META_REPLACE, "Smoothing sigma for detections",
-                     SIGMA_SMTH);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "SIGMA_SMOOTH", PS_META_REPLACE, "Smoothing sigma for detections", SIGMA_SMTH);
 
     // smooth the image, applying the mask as we go
@@ -56,6 +59,5 @@
     // effective per-pixel variance is maintained.
     psImage *smooth_wt = psImageCopy(NULL, readout->variance, PS_TYPE_F32);
-    psImageSmoothMask_Threaded(smooth_wt, smooth_wt, readout->mask, maskVal, SIGMA_SMTH * M_SQRT1_2,
-                      NSIGMA_SMTH, minGauss);
+    psImageSmoothMask_Threaded(smooth_wt, smooth_wt, readout->mask, maskVal, SIGMA_SMTH * M_SQRT1_2, NSIGMA_SMTH, minGauss);
     psLogMsg("psphot", PS_LOG_MINUTIA, "smooth variance: %f sec\n", psTimerMark("psphot.smooth"));
 
@@ -74,14 +76,11 @@
     // Calculate correction factor for the covariance produced by the (potentially multiple) smoothing
     psKernel *kernel = psImageSmoothKernel(SIGMA_SMTH, NSIGMA_SMTH); // Kernel used for smoothing
-    psKernel *covar = psImageCovarianceCalculate(kernel, readout->covariance); // Covariance matrix
+    float factor = 1.0 / psImageCovarianceCalculateFactor(kernel, readout->covariance);
     psFree(kernel);
-    float factor = 1.0 / psImageCovarianceFactor(covar);
-    psFree(covar);
 
     // record the effective area and significance scaling factor
     float effArea = 8.0 * M_PI * PS_SQR(SIGMA_SMTH);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "EFFECTIVE_AREA", PS_META_REPLACE, "Effective Area", effArea);
-    psMetadataAddF32(recipe, PS_LIST_TAIL, "SIGNIFICANCE_SCALE_FACTOR", PS_META_REPLACE,
-                     "Signicance scale factor", factor);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "EFFECTIVE_AREA", PS_META_REPLACE, "Effective Area", effArea);
+    psMetadataAddF32(readout->analysis, PS_LIST_TAIL, "SIGNIFICANCE_SCALE_FACTOR", PS_META_REPLACE, "Signicance scale factor", factor);
 
     // XXX multithread this if needed
Index: branches/simtest_nebulous_branches/psphot/src/psphotSkyReplace.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSkyReplace.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSkyReplace.c	(revision 27840)
@@ -1,13 +1,38 @@
 # include "psphotInternal.h"
+
+bool psphotSkyReplace (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (i == chisqNum) continue; // skip chisq image
+	if (!psphotSkyReplaceReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to replace sky for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
 
 // XXX make this an option?
 // in order to  successfully replace the sky, we must define a corresponding file...
-bool psphotSkyReplace (pmConfig *config, const pmFPAview *view) {
+bool psphotSkyReplaceReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index) {
 
     psTimerStart ("psphot.skyreplace");
 
     // find the currently selected readout
-    pmReadout *readout = pmFPAfileThisReadout (config->files, view, "PSPHOT.INPUT");
-    if (readout == NULL) psAbort("input not defined");
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
 
     // select background pixels, from output background file, or create
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourceFits.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourceFits.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourceFits.c	(revision 27840)
@@ -90,6 +90,11 @@
     psphotCheckRadiusPSFBlend (readout, source, PSF, markVal, dR);
 
-    // fit PSF model (set/unset the pixel mask)
+    // fit PSF model
     pmSourceFitSet (source, modelSet, PM_SOURCE_FIT_PSF, maskVal);
+
+    // clear the circular mask
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); 
+
+    if (!isfinite(PSF->params->data.F32[PM_PAR_I0])) psAbort("nan in fit");
 
     // correct model chisq for flux trend
@@ -101,4 +106,6 @@
         pmSource *blend = sourceSet->data[i];
         pmModel *model  = modelSet->data[i];
+
+	if (!isfinite(model->params->data.F32[PM_PAR_I0])) psAbort("nan in fit");
 
         // correct model chisq for flux trend
@@ -120,5 +127,4 @@
         pmSourceCacheModel (blend, maskVal);
         pmSourceSub (blend, PM_MODEL_OP_FULL, maskVal);
-        blend->tmpFlags |=  PM_SOURCE_TMPF_SUBTRACTED;
         blend->mode |=  PM_SOURCE_MODE_BLEND_FIT;
     }
@@ -144,5 +150,4 @@
     pmSourceCacheModel (source, maskVal);
     pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-    source->tmpFlags |=  PM_SOURCE_TMPF_SUBTRACTED;
     source->mode |=  PM_SOURCE_MODE_BLEND_FIT;
     return true;
@@ -167,4 +172,9 @@
     // fit PSF model (set/unset the pixel mask)
     pmSourceFitModel (source, PSF, PM_SOURCE_FIT_PSF, maskVal);
+
+    if (!isfinite(PSF->params->data.F32[PM_PAR_I0])) psAbort("nan in fit");
+
+    // clear the circular mask
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); 
 
     // correct model chisq for flux trend
@@ -186,10 +196,7 @@
     pmSourceCacheModel (source, maskVal);
     pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-    source->tmpFlags |=  PM_SOURCE_TMPF_SUBTRACTED;
-    return true;
-}
-
-static float EXT_MIN_SN;
-static float EXT_MOMENTS_RAD;
+    return true;
+}
+
 static pmModelType modelTypeEXT;
 
@@ -197,8 +204,4 @@
 
     bool status;
-
-    // extended source model parameters
-    EXT_MIN_SN       = psMetadataLookupF32 (&status, recipe, "EXT_MIN_SN");
-    EXT_MOMENTS_RAD  = psMetadataLookupF32 (&status, recipe, "EXT_MOMENTS_RADIUS");
 
     // extended source model descriptions
@@ -216,4 +219,7 @@
     double chiTrend;
     pmModel *ONE = NULL;
+    pmSource *tmpSrc = NULL;
+    pmModel *EXT = NULL;
+    psArray *DBL = NULL;
 
     // skip the source if we don't think it is extended
@@ -221,9 +227,13 @@
     if (source->type == PM_SOURCE_TYPE_DEFECT) return false;
     if (source->type == PM_SOURCE_TYPE_SATURATED) return false;
-    if (source->peak->SN < EXT_MIN_SN) return false;
-
+
+    // set the radius based on the footprint (also sets the mask pixels)
+    float radius = psphotSetRadiusEXT (readout, source, markVal);
+
+    // XXX note that this changes the source moments that are published...
     // recalculate the source moments using the larger extended-source moments radius
     // at this stage, skip Gaussian windowing, and do not clip pixels by S/N
-    if (!pmSourceMoments (source, EXT_MOMENTS_RAD, 0.0, 0.0)) return false;
+    // this uses the footprint to judge both radius and aperture?
+    if (!pmSourceMoments (source, radius, 0.0, 0.0, maskVal)) return false;
 
     psTrace ("psphot", 5, "trying blob...\n");
@@ -233,16 +243,25 @@
 
     // this temporary source is used as a place-holder by the psphotEval functions below
-    pmSource *tmpSrc = pmSourceAlloc ();
+    tmpSrc = pmSourceAlloc ();
 
     // XXX need to handle failures better here
-    pmModel *EXT = psphotFitEXT (readout, source, modelTypeEXT, maskVal, markVal);
+    EXT = psphotFitEXT (readout, source, modelTypeEXT, maskVal, markVal);
+    if (!EXT) goto escape;
+    if (!isfinite(EXT->params->data.F32[PM_PAR_I0])) goto escape;
+
     okEXT = psphotEvalEXT (tmpSrc, EXT);
     chiEXT = EXT ? EXT->chisq / EXT->nDOF : NAN;
 
     // DBL will always be defined, but DBL->data[n] might not
-    psArray *DBL = psphotFitDBL (readout, source, maskVal, markVal);
+    DBL = psphotFitDBL (readout, source, maskVal, markVal);
+    if (!DBL) goto escape;
+    if (!DBL->n) goto escape;
+
     okDBL  = psphotEvalDBL (tmpSrc, DBL->data[0]);
     okDBL &= psphotEvalDBL (tmpSrc, DBL->data[1]);
     // XXX should I keep / save the flags set in the eval functions?
+
+    // clear the circular mask
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); 
 
     // correct first model chisqs for flux trend
@@ -250,4 +269,5 @@
     ONE = DBL->data[0];
     if (ONE) {
+	if (!isfinite(ONE->params->data.F32[PM_PAR_I0])) psAbort("nan in fit");
       chiTrend = psPolynomial1DEval (psf->ChiTrend, ONE->params->data.F32[1]);
       ONE->chisqNorm = ONE->chisq / chiTrend;
@@ -258,4 +278,5 @@
     ONE = DBL->data[1];
     if (ONE) {
+	if (!isfinite(ONE->params->data.F32[PM_PAR_I0])) psAbort("nan in fit");
       chiTrend = psPolynomial1DEval (psf->ChiTrend, ONE->params->data.F32[1]);
       ONE->chisqNorm = ONE->chisq / chiTrend;
@@ -277,4 +298,5 @@
 
     // both models failed; reject them both
+    // XXX -- change type flags to psf in this case and keep original moments?
     psFree (EXT);
     psFree (DBL);
@@ -287,4 +309,5 @@
     // save new model
     source->modelEXT = EXT;
+    source->modelEXT->fitRadius = radius;
     source->type = PM_SOURCE_TYPE_EXTENDED;
     source->mode |= PM_SOURCE_MODE_EXTMODEL;
@@ -293,6 +316,15 @@
     pmSourceCacheModel (source, maskVal);
     pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
-    source->tmpFlags |=  PM_SOURCE_TMPF_SUBTRACTED;
+
+# if (PS_TRACE_ON)   
     psTrace ("psphot", 5, "blob as EXT: %f %f\n", EXT->params->data.F32[PM_PAR_XPOS], EXT->params->data.F32[PM_PAR_YPOS]);
+    if (psTraceGetLevel("psphot") >= 6) {
+	psLogMsg ("psphot", 1, "source 2:\n");
+	for (int i = 0; i < source->modelEXT->params->n; i++) {
+	    psLogMsg ("psphot", 1, "PAR %d : %f +/- %f\n", i, source->modelEXT->params->data.F32[i], source->modelEXT->dparams->data.F32[i]);
+	}
+    }
+# endif
+
     return true;
 
@@ -304,11 +336,11 @@
     psFree (source->modelPSF);
     source->modelPSF = psMemIncrRefCounter (DBL->data[0]);
-    source->tmpFlags |= PM_SOURCE_TMPF_SUBTRACTED;
     source->mode     |= PM_SOURCE_MODE_PAIR;
+    source->modelPSF->fitRadius = radius;
 
     // copy most data from the primary source (modelEXT, blends stay NULL)
-    // XXX use pmSourceCopy?
     pmSource *newSrc = pmSourceCopy (source);
     newSrc->modelPSF = psMemIncrRefCounter (DBL->data[1]);
+    newSrc->modelPSF->fitRadius = radius;
 
     // build cached models and subtract
@@ -317,5 +349,19 @@
     pmSourceCacheModel (newSrc, maskVal);
     pmSourceSub (newSrc, PM_MODEL_OP_FULL, maskVal);
+
+# if (PS_TRACE_ON)   
     psTrace ("psphot", 5, "blob as DBL: %f %f\n", ONE->params->data.F32[PM_PAR_XPOS], ONE->params->data.F32[PM_PAR_YPOS]);
+    if (psTraceGetLevel("psphot") >= 6) {
+	psLogMsg ("psphot", 1, "source 1:\n");
+	for (int i = 0; i < newSrc->modelPSF->params->n; i++) {
+	    psLogMsg ("psphot", 1, "PAR %d : %f +/- %f\n", i, newSrc->modelPSF->params->data.F32[i], newSrc->modelPSF->dparams->data.F32[i]);
+	}
+	psLogMsg ("psphot", 1, "source 2:\n");
+	for (int i = 0; i < source->modelPSF->params->n; i++) {
+	    psLogMsg ("psphot", 1, "PAR %d : %f +/- %f\n", i, source->modelPSF->params->data.F32[i], source->modelPSF->dparams->data.F32[i]);
+	}
+	psphotVisualShowResidualImage (readout);
+    }
+# endif
 
     psArrayAdd (newSources, 100, newSrc);
@@ -323,4 +369,11 @@
     psFree (DBL);
     return true;
+
+escape:
+    psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(markVal)); 
+    psFree (tmpSrc);
+    psFree (EXT);
+    psFree (DBL);
+    return false;
 }
 
@@ -356,5 +409,4 @@
     // save the PSF model from the Ensemble fit
     PSF = source->modelPSF;
-    psphotCheckRadiusPSFBlend (readout, source, PSF, markVal, 8.0);
     if (isnan(PSF->params->data.F32[1])) psAbort("nan in dbl fit");
 
@@ -387,9 +439,8 @@
     // use the source moments, etc to guess basic model parameters
     pmModel *EXT = pmSourceModelGuess (source, modelType);
-    PS_ASSERT (EXT, NULL);
-
-    // if (isnan(EXT->params->data.F32[1])) psAbort("nan in ext fit");
-
-    psphotCheckRadiusEXT (readout, source, EXT, markVal);
+    if (!EXT) {
+	psTrace ("psphot", 5, "failed to generate a model for source: moments: %f %f\n", source->moments->Mxx, source->moments->Myy);
+	return NULL;
+    }
 
     if ((source->moments->Mxx < 1e-3) || (source->moments->Myy < 1e-3)) {
@@ -401,3 +452,2 @@
     return (EXT);
 }
-
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourceFreePixels.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourceFreePixels.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourceFreePixels.c	(revision 27840)
@@ -1,7 +1,37 @@
 # include "psphotInternal.h"
 
-void psphotSourceFreePixels (psArray *sources) {
+bool psphotSourceFreePixels (pmConfig *config, const pmFPAview *view)
+{
+    bool status = true;
 
-    if (!sources) return;
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotSourceFreePixelsReadout (config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to free source pixels for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
+
+bool psphotSourceFreePixelsReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index) {
+
+    bool status;
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
 
     for (int i = 0; i < sources->n; i++) {
@@ -9,4 +39,4 @@
 	pmSourceFreePixels (source);
     }
-    return;
+    return true;
 }
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourceMatch.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourceMatch.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourceMatch.c	(revision 27840)
@@ -0,0 +1,256 @@
+# include "psphotInternal.h" 
+
+bool psphotMatchSourcesGenerate (pmConfig *config, const pmFPAview *view, psArray *objects);
+ 
+psArray *psphotMatchSources (pmConfig *config, const pmFPAview *view) 
+{
+    bool status = true;
+
+    psArray *objects = psArrayAllocEmpty(100);
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (!psphotMatchSourcesReadout (objects, config, view, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to merge sources for PSPHOT.INPUT entry %d", i);
+	    psFree (objects);
+            return NULL;
+        }
+    }
+
+    psphotMatchSourcesGenerate (config, view, objects);
+
+    return objects;
+}
+
+bool psphotMatchSourcesReadout (psArray *objects, pmConfig *config, const pmFPAview *view, char *filename, int index) { 
+ 
+    bool status = false;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int RADIUS = psMetadataLookupF32 (&status, recipe, "PSPHOT.STACK.MATCH.RADIUS");
+    psAssert (status, "programming error: must define PSPHOT.STACK.MATCH.RADIUS");
+
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+    psAssert (detections->newSources, "new sources not defined?");
+    psAssert (!detections->allSources, "all sources already defined?");
+
+    // XXX TEST:
+    if (detections->newSources) {
+        psphotMatchSourcesToObjects(objects, detections->newSources, RADIUS);
+    }
+
+    return true;
+}
+
+# define NEXT1 { i++; continue; } 
+# define NEXT2 { j++; continue; } 
+bool psphotMatchSourcesToObjects (psArray *objects, psArray *sources, float RADIUS) { 
+ 
+    float dx, dy; 
+ 
+    float RADIUS2 = RADIUS*RADIUS;
+
+    // sort the source list by X 
+    sources = psArraySort (sources, pmSourceSortByX); 
+    objects = psArraySort (objects, pmPhotObjSortByX); 
+ 
+    psVector *found = psVectorAlloc(sources->n, PS_TYPE_U8);
+    psVectorInit (found, 0);
+
+    // match sources to existing objects
+
+    psLogMsg ("psphot", PS_LOG_DETAIL, "attempt to match sources (%ld vs %ld)", sources->n, objects->n);
+
+    int i, j; 
+    for (i = j = 0; (i < sources->n) && (j < objects->n); ) { 
+ 
+        pmSource  *src = sources->data[i]; 
+        pmPhotObj *obj = objects->data[j]; 
+ 
+        if (!src) NEXT1; 
+        if (!src->peak) NEXT1; 
+        if (!finite(src->peak->xf)) NEXT1; 
+        if (!finite(src->peak->yf)) NEXT1; 
+ 
+        if (!obj) NEXT2; 
+        if (!finite(obj->x)) NEXT2; 
+        if (!finite(obj->y)) NEXT2; 
+ 
+        dx = src->peak->xf - obj->x; 
+        if (dx < -1.02*RADIUS) NEXT1; 
+        if (dx > +1.02*RADIUS) NEXT2; 
+ 
+        // we are within match range, look for matches: 
+	int Jmin = -1;
+	float Rmin = RADIUS2;
+        for (int J = j; (dx > -1.02*RADIUS) && (J < objects->n); J++) { 
+ 
+	    obj = objects->data[J]; 
+	    
+	    dx = src->peak->xf - obj->x; 
+            dy = src->peak->yf - obj->y; 
+ 
+            float dr = dx*dx + dy*dy; 
+            if (dr > RADIUS2) continue; 
+	    if (dr < Rmin) {
+		Rmin = dr;
+		Jmin  = J;
+	    }
+	}
+
+	// no match, try next source
+	if (Jmin == -1) {
+	    i++;
+	    continue;
+	}
+	obj = objects->data[Jmin]; 
+
+	// add to object
+	pmPhotObjAddSource (obj, src);
+	found->data.U8[i] = 1;
+        i++; 
+    } 
+
+    // add missed sources to new objects
+
+    for (i = 0; i < sources->n; i++) {
+
+	if (found->data.U8[i]) continue;
+
+        pmSource *src = sources->data[i]; 
+
+	pmPhotObj *obj = pmPhotObjAlloc();
+	pmPhotObjAddSource(obj, src);
+	psArrayAdd (objects, 100, obj);
+	psFree (obj);
+    }
+    psLogMsg ("psphot", PS_LOG_DETAIL, "matched sources (%ld vs %ld)", sources->n, objects->n);
+
+    psFree (found);
+    return true;
+} 
+
+bool psphotMatchSourcesGenerate (pmConfig *config, const pmFPAview *view, psArray *objects) {
+
+    bool status = false;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    // determine properties (sky, moments) of initial sources
+    float OUTER = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    psAssert (status, "missing SKY_OUTER_RADIUS in recipe?");
+
+    int nImages = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // generate look-up arrays for detections and readouts
+    psArray *detArrays = psArrayAlloc(nImages);
+    psArray *readouts = psArrayAlloc(nImages);
+
+    for (int i = 0; i < nImages; i++) {
+
+	// find the currently selected readout
+	pmFPAfile *file = pmFPAfileSelectSingle(config->files, "PSPHOT.INPUT", i); // File of interest
+	psAssert (file, "missing file?");
+
+	pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+	psAssert (readout, "missing readout?");
+
+	pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+	psAssert (detections, "missing detections?");
+
+	detArrays->data[i] = psMemIncrRefCounter(detections);
+	readouts->data[i] = psMemIncrRefCounter(readout);
+    }
+
+    // vector to track if source for an image is found
+    psVector *found = psVectorAlloc(nImages, PS_TYPE_U8);
+
+    for (int i = 0; i < objects->n; i++) { 
+        pmPhotObj *obj = objects->data[i]; 
+
+	// mark the images for which sources have been found
+	psVectorInit (found, 0);
+	for (int j = 0; j < obj->sources->n; j++) {
+
+	    pmSource *src = obj->sources->data[j]; 
+	    int index = src->imageID;
+	    psAssert (index >= 0, "invalid index");
+	    psAssert (index < found->n, "invalid index");
+
+	    found->data.U8[index] = 1;
+	}
+
+	// generate new sources for the image that are missing
+	for (int index = 0; index < found->n; index++) {
+	    if (found->data.U8[index]) continue;
+
+	    pmDetections *detections = detArrays->data[index];
+	    pmReadout *readout = readouts->data[index];
+	    int row0 = readout->image->row0;
+	    int col0 = readout->image->col0;
+
+	    // XXX the peak type is not really used in psphot
+	    // PM_PEAK_LONE is certainly not true, but irrelevant
+	    float peakFlux = readout->image->data.F32[(int)(obj->y-row0-0.5)][(int)(obj->x-col0-0.5)];
+	    pmPeak *peak = pmPeakAlloc(obj->x, obj->y, peakFlux, PM_PEAK_LONE);
+	    peak->flux = peakFlux;
+	    peak->SN = 1.0;
+	    peak->xf = obj->x;
+	    peak->yf = obj->y;
+	    peak->dx = NAN;
+	    peak->dy = NAN;
+	    
+	    // XXX assign to a footprint?
+
+	    // create a new source
+	    pmSource *source = pmSourceAlloc();
+	    source->imageID = index;
+
+	    // add the peak
+	    source->peak = psMemIncrRefCounter(peak);
+
+	    // allocate space for moments
+	    source->moments = pmMomentsAlloc();
+
+	    // allocate image, weight, mask arrays for each peak (square of radius OUTER)
+	    pmSourceDefinePixels (source, readout, source->peak->x, source->peak->y, OUTER);
+
+	    peak->assigned = true;
+	    pmPhotObjAddSource(obj, source);
+	    psArrayAdd (detections->newSources, 100, source);
+	    psFree (source);
+	    psFree (peak);
+	}
+    }
+
+    // how many sources do we have now?
+    int nSources = 0;
+    for (int i = 0; i < objects->n; i++) { 
+        pmPhotObj *obj = objects->data[i]; 
+	nSources += obj->sources->n;
+    }
+    psLogMsg ("psphot", PS_LOG_DETAIL, "total of %d sources for %d images", nSources, nImages);
+
+
+    psFree (found);
+    psFree (detArrays);
+    psFree (readouts);
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourcePlots.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourcePlots.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourcePlots.c	(revision 27840)
@@ -111,12 +111,14 @@
             if (Xo == 0) {
                 // place source alone on this row
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+		bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+		if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotRadialPlot (&kapa, "radial.plots.ps", source);
                 psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-                psphotSubWithTest (source, false, maskVal); // remove source (force)
+		pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
 
-                psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+		if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+
                 Yo += DY;
                 Xo = 0;
@@ -126,11 +128,13 @@
                 Yo += dY;
                 Xo = 0;
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+
+		bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+		if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotRadialPlot (&kapa, "radial.plots.ps", source);
                 psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-                psphotSubWithTest (source, false, maskVal); // remove source (force)
+		pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
-                psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+		if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
                 Xo = DX;
@@ -139,11 +143,12 @@
         } else {
             // extend this row
-            psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+	    bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+	    if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
             psphotRadialPlot (&kapa, "radial.plots.ps", source);
             psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-            psphotSubWithTest (source, false, maskVal); // remove source (force)
+	    pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
             psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
-            psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+	    if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
             Xo += DX;
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourceSize.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourceSize.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourceSize.c	(revision 27840)
@@ -2,9 +2,27 @@
 # include <gsl/gsl_sf_gamma.h>
 
-static float psphotModelContour(const psImage *image, const psImage *variance, const psImage *mask,
-                                psImageMaskType maskVal, const pmModel *model, float Ro);
-
-bool psphotMaskCosmicRay_Old (pmSource *source, psImageMaskType maskVal, psImageMaskType crMask);
-bool psphotMaskCosmicRay_New (psImage *mask, pmSource *source, psImageMaskType maskVal, psImageMaskType crMask);
+typedef struct {
+    psImageMaskType maskVal;
+    psImageMaskType markVal;
+    psImageMaskType crMask;
+    float ApResid;
+    float ApSysErr;
+    float nSigmaApResid;
+    float nSigmaMoments;
+    float nSigmaCR;
+    float soft;
+    int grow;
+    int xtest, ytest;
+    bool apply; // apply CR mask?
+} psphotSourceSizeOptions;
+
+// local functions:
+bool psphotSourceSizePSF (psphotSourceSizeOptions *options, psArray *sources, pmPSF *psf);
+bool psphotSourceClass (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psphotSourceSizeOptions *options);
+bool psphotSourceClassRegion (psRegion *region, pmPSFClump *psfClump, psArray *sources, psMetadata *recipe, pmPSF *psf, psphotSourceSizeOptions *options);
+bool psphotSourceSizeCR (pmReadout *readout, psArray *sources, psphotSourceSizeOptions *options);
+bool psphotMaskCosmicRay (pmReadout *readout, pmSource *source, psImageMaskType maskVal);
+bool psphotMaskCosmicRayFootprintCheck (psArray *sources);
+int  psphotMaskCosmicRayConnected (int xPeak, int yPeak, psImage *mymask, psImage *myvar, psImage *edges, int binning, float sigma_thresh);
 
 // we need to call this function after sources have been fitted to the PSF model and
@@ -14,42 +32,276 @@
 // deviation from the psf model at the r = FWHM/2 position
 
-bool psphotSourceSize(pmConfig *config, pmReadout *readout, psArray *sources, psMetadata *recipe, long first)
+// for now, let's store the detections on the readout->analysis for each readout
+bool psphotSourceSize (pmConfig *config, const pmFPAview *view, bool getPSFsize)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // skip the chisq image (optionally?)
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    if (!status) chisqNum = -1;
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (i == chisqNum) continue; // skip chisq image
+        if (!psphotSourceSizeReadout (config, view, "PSPHOT.INPUT", i, recipe, getPSFsize)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed on source size analysis for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
+
+// this function use an internal flag to mark sources which have already been measured
+bool psphotSourceSizeReadout(pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool getPSFsize)
 {
     bool status;
+    psphotSourceSizeOptions options;
 
     psTimerStart ("psphot.size");
 
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    psArray *sources = detections->allSources;
+    psAssert (sources, "missing sources?");
+
+    if (!sources->n) {
+        psLogMsg ("psphot", PS_LOG_INFO, "no sources, skipping source size");
+        return true;
+    }
+
+    pmPSF *psf = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.PSF");
+    psAssert (psf, "missing psf?");
+
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
-    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
-    assert (maskVal);
+    options.maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
+    assert (options.maskVal);
+
+    options.markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT"); // Mask value for bad pixels
+    assert (options.markVal);
 
     // bit to mask the cosmic-ray pixels
-    psImageMaskType crMask  = pmConfigMaskGet("CR", config); // Mask value for cosmic rays
-
-    float CR_NSIGMA_LIMIT = psMetadataLookupF32 (&status, recipe, "PSPHOT.CR.NSIGMA.LIMIT");
+    options.crMask  = pmConfigMaskGet("CR", config); // Mask value for cosmic rays
+
+    options.nSigmaCR = psMetadataLookupF32 (&status, recipe, "PSPHOT.CR.NSIGMA.LIMIT");
     assert (status);
 
-    float EXT_NSIGMA_LIMIT = psMetadataLookupF32 (&status, recipe, "PSPHOT.EXT.NSIGMA.LIMIT");
+    // XXX recipe name is not great
+    options.nSigmaApResid = psMetadataLookupF32 (&status, recipe, "PSPHOT.EXT.NSIGMA.LIMIT");
     assert (status);
 
-    int grow = psMetadataLookupS32(&status, recipe, "PSPHOT.CR.GROW"); // Growth size for CRs
-    if (!status || grow < 0) {
+    // XXX recipe name is not great
+    options.nSigmaMoments = psMetadataLookupF32 (&status, recipe, "PSPHOT.EXT.NSIGMA.MOMENTS");
+    assert (status);
+
+    // XXX recipe name is not great
+    options.xtest = psMetadataLookupS32 (&status, recipe, "PSPHOT.CRMASK.XTEST");
+    options.ytest = psMetadataLookupS32 (&status, recipe, "PSPHOT.CRMASK.YTEST");
+    assert (status);
+
+    options.grow = psMetadataLookupS32(&status, recipe, "PSPHOT.CR.GROW"); // Growth size for CRs
+    if (!status || options.grow < 0) {
         psError(PS_ERR_BAD_PARAMETER_VALUE, true, "PSPHOT.CR.GROW is not positive.");
         return false;
     }
 
-    float soft = psMetadataLookupF32(&status, recipe, "PSPHOT.CR.NSIGMA.SOFTEN"); // Softening parameter
-    if (!status || !isfinite(soft) || soft < 0.0) {
+    options.soft = psMetadataLookupF32(&status, recipe, "PSPHOT.CR.NSIGMA.SOFTEN"); // Softening parameter
+    if (!status || !isfinite(options.soft) || options.soft < 0.0) {
         psWarning("PSPHOT.CR.NSIGMA.SOFTEN not set; defaulting to zero.");
-        soft = 0.0;
-    }
-
-    // loop over all source
-    for (int i = first; i < sources->n; i++) {
+        options.soft = 0.0;
+    }
+
+    options.apply = psMetadataLookupBool(&status, recipe, "PSPHOT.CRMASK.APPLY"); // Growth size for CRs
+    if (!status) {
+        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "PSPHOT.CRMASK.APPLY is not defined.");
+        return false;
+    }
+
+    // We are using the value psfMag - 2.5*log10(moment.Sum) as a measure of the extendedness
+    // of an object.  We need to model this distribution for the PSF stars before we can test
+    // the significance for a specific object
+    // XXX move this to the code that generates the PSF?
+    // XXX store the results on pmPSF?
+
+    // XXX this should only be done on the first pass (ie, if we have newSources or allSources?)
+    if (getPSFsize) {
+        psphotSourceSizePSF (&options, sources, psf);
+    }
+
+    // classify the sources based on ApResid and Moments (extended sources)
+    // NOTE: only sources not already measured !(source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED)
+    psphotSourceClass(readout, sources, recipe, psf, &options);
+
+    // NOTE: only sources not already measured !(source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED)
+    psphotSourceSizeCR (readout, sources, &options);
+
+    // XXX fix this (was source->n  - first)
+    psLogMsg ("psphot.size", PS_LOG_INFO, "measure source sizes for %ld sources: %f sec\n", sources->n, psTimerMark ("psphot.size"));
+
+    psphotVisualPlotSourceSize (recipe, readout->analysis, sources);
+    psphotVisualShowSourceSize (readout, sources);
+    psphotVisualPlotApResid (sources, options.ApResid, options.ApSysErr);
+    psphotVisualShowSatStars (recipe, psf, sources);
+
+    return true;
+}
+
+// model the apmifit distribution for the psf stars:
+bool psphotSourceSizePSF (psphotSourceSizeOptions *options, psArray *sources, pmPSF *psf) {
+
+    // select stats from the psf stars
+    psVector *Ap = psVectorAllocEmpty (100, PS_TYPE_F32);
+    psVector *ApErr = psVectorAllocEmpty (100, PS_TYPE_F32);
+
+    psImageMaskType maskVal = options->maskVal | options->markVal;
+
+    // XXX  why PHOT_WEIGHT??
+    pmSourcePhotometryMode photMode = PM_SOURCE_PHOT_WEIGHT;
+
+    int num = 0;                        // Number of sources measured
+    for (int i = 0; i < sources->n; i++) {
         pmSource *source = sources->data[i];
+        if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
+        num++;
+
+        // replace object in image
+        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, options->maskVal);
+        }
+
+        // clear the mask bit and set the circular mask pixels
+        psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(options->markVal));
+        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, source->apRadius, "OR", options->markVal);
+
+        // XXX can we test if psfMag is set and calculate only if needed?
+        pmSourceMagnitudes (source, psf, photMode, maskVal); // maskVal includes markVal
+
+        // clear the mask bit
+        psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(options->markVal));
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, options->maskVal);
+
+        float apMag = -2.5*log10(source->moments->Sum);
+        float dMag = source->psfMag - apMag;
+
+        psVectorAppend (Ap, dMag);
+        psVectorAppend (ApErr, source->errMag);
+    }
+    if (num == 0) {
+        // Not raising an error, because errors aren't being checked elsewhere in this function
+        psFree(Ap);
+        psFree(ApErr);
+        return false;
+    }
+
+    // model the distribution as a mean or median value and a systematic error from that value:
+    psStats *stats = psStatsAlloc(PS_STAT_ROBUST_MEDIAN);
+    psVectorStats (stats, Ap, NULL, NULL, 0);
+
+    psVector *dAp = psVectorAlloc (Ap->n, PS_TYPE_F32);
+    for (int i = 0; i < Ap->n; i++) {
+        dAp->data.F32[i] = Ap->data.F32[i] - stats->robustMedian;
+    }
+
+    options->ApResid = stats->robustMedian;
+    options->ApSysErr = psVectorSystematicError(dAp, ApErr, 0.05);
+    // XXX this is quite arbitrary...
+    if (!isfinite(options->ApSysErr)) options->ApSysErr = 0.01;
+    psLogMsg ("psphot", PS_LOG_DETAIL, "psf - Sum: %f +/- %f\n", options->ApResid, options->ApSysErr);
+
+    psFree (Ap);
+    psFree (ApErr);
+    psFree (stats);
+    psFree (dAp);
+
+    return true;
+}
+
+// classify sources based on the combination of psf-mag, Mxx, Myy
+bool psphotSourceClass (pmReadout *readout, psArray *sources, psMetadata *recipe, pmPSF *psf, psphotSourceSizeOptions *options) {
+
+    bool status;
+    pmPSFClump psfClump;
+    char regionName[64];
+
+    psLogMsg("psModules.objects", PS_LOG_INFO, "Source Size classifications: %4s %4s %4s %4s %4s %4s", "Npsf", "Next", "Nsat", "Ncr", "Nmiss", "Nskip");
+
+    int nRegions = psMetadataLookupS32 (&status, readout->analysis, "PSF.CLUMP.NREGIONS");
+    for (int i = 0; i < nRegions; i ++) {
+        snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", i);
+        psMetadata *regionMD = psMetadataLookupPtr (&status, readout->analysis, regionName);
+        psAssert (regionMD, "regions must be defined by earlier call to psphotRoughClassRegion");
+
+        psRegion *region = psMetadataLookupPtr (&status, regionMD, "REGION");
+        psAssert (region, "regions must be defined by earlier call to psphotRoughClassRegion");
+
+        // pull FWHM_X,Y from the recipe, use to define psfClump.X,Y
+        psfClump.X  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");   psAssert (status, "missing PSF.CLUMP.X");
+        psfClump.Y  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");   psAssert (status, "missing PSF.CLUMP.Y");
+        psfClump.dX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");  psAssert (status, "missing PSF.CLUMP.DX");
+        psfClump.dY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");  psAssert (status, "missing PSF.CLUMP.DY");
+
+        if ((psfClump.X < 0) || (psfClump.Y < 0) || !psfClump.X || !psfClump.Y || isnan(psfClump.X) || isnan(psfClump.Y)) {
+            psLogMsg ("psphot", 4, "Failed to find a valid PSF clump for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
+            continue;
+        }
+
+        if (!psphotSourceClassRegion (region, &psfClump, sources, recipe, psf, options)) {
+            psLogMsg ("psphot", 4, "Failed to determine source classification for region %f,%f - %f,%f\n", region->x0, region->y0, region->x1, region->y1);
+            continue;
+        }
+        // psphotVisualPlotSourceSize (recipe, readout->analysis, sources);
+    }
+
+    return true;
+}
+
+# define SIZE_SN_LIM 10
+bool psphotSourceClassRegion (psRegion *region, pmPSFClump *psfClump, psArray *sources, psMetadata *recipe, pmPSF *psf, psphotSourceSizeOptions *options) {
+
+    PS_ASSERT_PTR_NON_NULL(sources, false);
+    PS_ASSERT_PTR_NON_NULL(recipe, false);
+
+    int Nsat  = 0;
+    int Next  = 0;
+    int Npsf  = 0;
+    int Ncr   = 0;
+    int Nmiss = 0;
+    int Nskip = 0;
+
+    pmSourceMode noMoments = PM_SOURCE_MODE_MOMENTS_FAILURE | PM_SOURCE_MODE_SKYVAR_FAILURE | PM_SOURCE_MODE_SKY_FAILURE | PM_SOURCE_MODE_BELOW_MOMENTS_SN;
+    pmSourcePhotometryMode photMode = PM_SOURCE_PHOT_WEIGHT;
+
+    psImageMaskType maskVal = options->maskVal | options->markVal;
+
+    for (psS32 i = 0 ; i < sources->n ; i++) {
+
+        pmSource *source = (pmSource *) sources->data[i];
+
+        // psfClumps are found for image subregions:
+        // skip sources not in this region
+        if (source->peak->x <  region->x0) continue;
+        if (source->peak->x >= region->x1) continue;
+        if (source->peak->y <  region->y0) continue;
+        if (source->peak->y >= region->y1) continue;
 
         // skip source if it was already measured
-        if (isfinite(source->crNsigma)) {
-            psTrace("psphot", 7, "Not calculating extNsigma,crNsigma since already measured\n");
+        if (source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED) {
+            psTrace("psphot", 7, "Not calculating source size since it has already been measured\n");
             continue;
         }
@@ -57,21 +309,155 @@
         // source must have been subtracted
         if (!(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED)) {
-	    source->mode |= PM_SOURCE_MODE_SIZE_SKIPPED;
-            psTrace("psphot", 7, "Not calculating extNsigma,crNsigma since source is not subtracted\n");
-            continue;
-        }
-
-        psF32 **resid  = source->pixels->data.F32;
-        psF32 **variance = source->variance->data.F32;
-        psImageMaskType **mask = source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA;
-
-        // check for extendedness: measure the delta flux significance at the 1 sigma contour
-        source->extNsigma = psphotModelContour(source->pixels, source->variance, source->maskObj, maskVal,
-                                               source->modelPSF, 1.0);
-
-        // XXX prevent a source from being both CR and EXT?
-        if (source->extNsigma > EXT_NSIGMA_LIMIT) {
+            source->mode |= PM_SOURCE_MODE_SIZE_SKIPPED;
+            psTrace("psphot", 7, "Not calculating source size since source is not subtracted\n");
+            continue;
+        }
+
+        // we are basically classifying by moments
+        psAssert (source->moments, "why is this source missing moments?");
+        if (source->mode & noMoments) {
+            Nskip ++;
+            continue;
+        }
+
+        // convert to Mmaj, Mmin:
+        psF32 Mxx = source->moments->Mxx;
+        psF32 Myy = source->moments->Myy;
+
+        // replace object in image
+        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, options->maskVal);
+        }
+
+        // clear the mask bit and set the circular mask pixels
+        psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(options->markVal));
+        psImageKeepCircle (source->maskObj, source->peak->x, source->peak->y, source->apRadius, "OR", options->markVal);
+
+        // XXX can we test if psfMag is set and calculate only if needed?
+        pmSourceMagnitudes (source, psf, photMode, maskVal); // maskVal includes markVal
+
+        // clear the mask bit
+        psImageMaskPixels (source->maskObj, "AND", PS_NOT_IMAGE_MASK(options->markVal));
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, options->maskVal);
+
+        float apMag = -2.5*log10(source->moments->Sum);
+        float dMag = source->psfMag - apMag;
+
+        // set nSigma to include both systematic and poisson error terms
+        // XXX the 'poisson error' contribution for size is probably wrong...
+        float nSigmaMAG = (dMag - options->ApResid) / hypot(source->errMag, options->ApSysErr);
+        float nSigmaMXX = (Mxx - psfClump->X) / hypot(psfClump->dX, psfClump->X*psfClump->X*source->errMag);
+        float nSigmaMYY = (Myy - psfClump->Y) / hypot(psfClump->dY, psfClump->Y*psfClump->Y*source->errMag);
+
+        // partially-masked sources are more likely to be mis-measured PSFs
+        float sizeBias = 1.0;
+        if (source->pixWeight < 0.9) {
+            sizeBias = 3.0;
+        }
+
+        float minMxx = psfClump->X - sizeBias*options->nSigmaMoments*psfClump->dX;
+        float minMyy = psfClump->Y - sizeBias*options->nSigmaMoments*psfClump->dY;
+
+        // include MAG, MXX, and MYY?
+        source->extNsigma = nSigmaMAG;
+
+        // notes to clarify the source size classification rules:
+        // * a defect should be functionally equivalent to a cosmic ray
+        // * CR & defect should have a faintess limit (min S/N)
+        // * SAT stars should not be faint, but defects may?
+
+        // Anything within this region is a probably PSF-like object. Saturated stars may land
+        // in this region, but are detected elsewhere on the basis of their peak value.
+        bool isPSF = (fabs(nSigmaMAG) < options->nSigmaApResid) && (fabs(nSigmaMXX) < sizeBias*options->nSigmaMoments) && (fabs(nSigmaMYY) < sizeBias*options->nSigmaMoments);
+        if (isPSF) {
+          psTrace("psphotSourceClassRegion.PSF",4,"CLASS: %g %g\t%g %g  %g %g  %g %g\t%g %g\t%g PSF\t%g %g\n",
+                  source->peak->xf,source->peak->yf,Mxx,Myy,psfClump->X,psfClump->Y,psfClump->dX,psfClump->dY,apMag,dMag,nSigmaMAG,
+                  options->nSigmaApResid,sizeBias*options->nSigmaMoments);
+          source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+          Npsf ++;
+          continue;
+        }
+
+        // Defects may not always match CRs from peak curvature analysis
+        // Defects may also be marked as SATSTAR -- XXX deactivate this flag?
+        // XXX this rule is not great
+        // XXX only accept brightish detections as CRs
+        // (nSigmaMAG < -options->nSigmaApResid) ||
+        bool isCR = isCR = (source->errMag < 1.0 / SIZE_SN_LIM) && ((Mxx < minMxx) || (Myy < minMyy));
+        if (isCR) {
+            psTrace("psphotSourceClassRegion.CR",4,"CLASS: %g %g %f\t%g %g  %g %g  %g %g\t%g %g\t%g CR\t%g %g\n",
+                    source->peak->xf,source->peak->yf,source->pixWeight,Mxx,Myy,psfClump->X,psfClump->Y,psfClump->dX,psfClump->dY,apMag,dMag,nSigmaMAG,
+                    options->nSigmaApResid,sizeBias*options->nSigmaMoments);
+            source->mode |= PM_SOURCE_MODE_DEFECT;
+            source->tmpFlags |= PM_SOURCE_TMPF_SIZE_CR_CANDIDATE;
+            Ncr ++;
+            continue;
+        }
+
+        // saturated star (determined in PSF fit).  These may also be saturated galaxies, or
+        // just large saturated regions.
+        if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+            psTrace("psphotSourceClassRegion.SAT",4,"CLASS: %g %g\t%g %g  %g %g  %g %g\t%g %g\t%g SAT\t%g %g\n",
+                    source->peak->xf,source->peak->yf,Mxx,Myy,psfClump->X,psfClump->Y,psfClump->dX,psfClump->dY,apMag,dMag,nSigmaMAG,
+                    options->nSigmaApResid,sizeBias*options->nSigmaMoments);
+            source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+            Nsat ++;
+            continue;
+        }
+
+        // XXX allow the Mxx, Myy to be less than psfClump->X,Y (by some nSigma)?
+        bool isEXT = (nSigmaMAG > options->nSigmaApResid) || (Mxx > minMxx) || (Myy > minMyy);
+        if (isEXT) {
+          psTrace("psphotSourceClassRegion.EXT",4,"CLASS: %g %g\t%g %g  %g %g  %g %g\t%g %g\t%g Ext\t%g %g\n",
+                  source->peak->xf,source->peak->yf,Mxx,Myy,psfClump->X,psfClump->Y,psfClump->dX,psfClump->dY,apMag,dMag,nSigmaMAG,
+                  options->nSigmaApResid,sizeBias*options->nSigmaMoments);
+
             source->mode |= PM_SOURCE_MODE_EXT_LIMIT;
-        }
+            source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+            Next ++;
+            continue;
+        }
+        psTrace("psphotSourceClassRegion.MISS",4,"CLASS: %g %g\t%g %g  %g %g  %g %g\t%g %g\t%g Unk\t%g %g\n",
+                source->peak->xf,source->peak->yf,Mxx,Myy,psfClump->X,psfClump->Y,psfClump->dX,psfClump->dY,apMag,dMag,nSigmaMAG,
+                options->nSigmaApResid,sizeBias*options->nSigmaMoments);
+
+        // sources that reach here are probably too faint for a reasonable source size measurement
+        // psWarning ("sourse size was missed for %f,%f : %f %f -- %f\n", source->peak->xf, source->peak->yf, Mxx, Myy, nSigmaMAG);
+        source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+        Nmiss ++;
+    }
+
+    psLogMsg("psModules.objects", PS_LOG_INFO, "Source Size classifications: %4d %4d %4d %4d %4d %4d", Npsf, Next, Nsat, Ncr, Nmiss, Nskip);
+
+    return true;
+}
+
+// given an object suspected to be a defect, generate a pixel mask using the Lapacian transform
+// if enough of the object is detected as 'sharp', consider the object a cosmic ray
+bool psphotSourceSizeCR (pmReadout *readout, psArray *sources, psphotSourceSizeOptions *options) {
+
+    psTimerStart ("psphot.cr");
+
+    int nMasked = 0;
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // skip source if it was already measured
+        if (source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED) {
+            psTrace("psphot", 7, "Not calculating source size since it has already been measured\n");
+            continue;
+        }
+
+        // only check candidates marked above
+        if (!(source->tmpFlags & PM_SOURCE_TMPF_SIZE_CR_CANDIDATE)) {
+            psTrace("psphot", 7, "Not calculating source size since it has already been measured\n");
+            continue;
+        }
+
+        // skip unless this source is thought to be a cosmic ray.  flag the detection and mask the pixels
+        // XXX this may be degenerate with the above test
+        if (!(source->mode & PM_SOURCE_MODE_DEFECT)) continue;
 
         // Integer position of peak
@@ -79,99 +465,37 @@
         int yPeak = source->peak->yf - source->pixels->row0 + 0.5;
 
-        // XXX for now, skip sources which are too close to a boundary
-        // XXX raise a flag?
+        // Skip sources which are too close to a boundary.  These are mostly caught as DEFECT
         if (xPeak < 1 || xPeak > source->pixels->numCols - 2 ||
             yPeak < 1 || yPeak > source->pixels->numRows - 2) {
-	    source->mode |= PM_SOURCE_MODE_SIZE_SKIPPED;
             psTrace("psphot", 7, "Not calculating crNsigma due to edge\n");
             continue;
         }
 
-        // XXX for now, just skip any sources with masked pixels
-        // XXX raise a flag?
-        bool keep = true;
-        for (int iy = -1; (iy <= +1) && keep; iy++) {
-            for (int ix = -1; (ix <= +1) && keep; ix++) {
-                if (mask[yPeak+iy][xPeak+ix] & maskVal) {
-                    keep = false;
-                }
-            }
-        }
-        if (!keep) {
-            psTrace("psphot", 7, "Not calculating crNsigma due to masked pixels\n");
-	    source->mode |= PM_SOURCE_MODE_SIZE_SKIPPED;
-            continue;
-        }
-
-        // Compare the central pixel with those on either side, for the four possible lines through it.
-
-        // Soften variances (add systematic error)
-        float softening = soft * PS_SQR(source->peak->flux); // Softening for variances
-
-        // Across the middle: y = 0
-        float cX = 2*resid[yPeak][xPeak]   - resid[yPeak+0][xPeak-1]  - resid[yPeak+0][xPeak+1];
-        float dcX = 4*variance[yPeak][xPeak] + variance[yPeak+0][xPeak-1] + variance[yPeak+0][xPeak+1];
-        float nX = cX / sqrtf(dcX + softening);
-
-        // Up the centre: x = 0
-        float cY = 2*resid[yPeak][xPeak]   - resid[yPeak-1][xPeak+0]  - resid[yPeak+1][xPeak+0];
-        float dcY = 4*variance[yPeak][xPeak] + variance[yPeak-1][xPeak+0] + variance[yPeak+1][xPeak+0];
-        float nY = cY / sqrtf(dcY + softening);
-
-        // Diagonal: x = y
-        float cL = 2*resid[yPeak][xPeak]   - resid[yPeak-1][xPeak-1]  - resid[yPeak+1][xPeak+1];
-        float dcL = 4*variance[yPeak][xPeak] + variance[yPeak-1][xPeak-1] + variance[yPeak+1][xPeak+1];
-        float nL = cL / sqrtf(dcL + softening);
-
-        // Diagonal: x = - y
-        float cR = 2*resid[yPeak][xPeak]   - resid[yPeak+1][xPeak-1]  - resid[yPeak-1][xPeak+1];
-        float dcR = 4*variance[yPeak][xPeak] + variance[yPeak+1][xPeak-1] + variance[yPeak-1][xPeak+1];
-        float nR = cR / sqrtf(dcR + softening);
-
-        // P(chisq > chisq_obs; Ndof) = gamma_Q (Ndof/2, chisq/2)
-        // Ndof = 4 ? (four measurements, no free parameters)
-        // XXX this value is going to be biased low because of systematic errors.
-        // we need to calibrate it somehow
-        // source->psfProb = gsl_sf_gamma_inc_Q (2, 0.5*chisq);
-
-        // not strictly accurate: overcounts the chisq contribution from the center pixel (by
-        // factor of 4); also biases a bit low if any pixels are masked
-        // XXX I am not sure I want to keep this value...
-        source->psfChisq = PS_SQR(nX) + PS_SQR(nY) + PS_SQR(nL) + PS_SQR(nR);
-
-        float fCR = 0.0;
-        int nCR = 0;
-        if (nX > 0.0) {
-            fCR += nX;
-            nCR ++;
-        }
-        if (nY > 0.0) {
-            fCR += nY;
-            nCR ++;
-        }
-        if (nL > 0.0) {
-            fCR += nL;
-            nCR ++;
-        }
-        if (nR > 0.0) {
-            fCR += nR;
-            nCR ++;
-        }
-        source->crNsigma  = (nCR > 0)  ? fCR / nCR : 0.0;
-        if (!isfinite(source->crNsigma)) {
-	    continue;
-	}
-
-        // this source is thought to be a cosmic ray.  flag the detection and mask the pixels
-        if (source->crNsigma > CR_NSIGMA_LIMIT) {
-            // XXX still testing... : psphotMaskCosmicRay_New (readout->mask, source, maskVal, crMask);
-            psphotMaskCosmicRay_Old (source, maskVal, crMask);
-        }
+        // XXX for testing, only CRMASK a single source:
+        if (options->xtest && (fabs(source->peak->xf - options->xtest) > 5)) continue;
+        if (options->ytest && (fabs(source->peak->yf - options->ytest) > 5)) continue;
+
+        // replace object in image
+        if (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED) {
+            pmSourceAdd (source, PM_MODEL_OP_FULL, options->maskVal);
+        }
+
+        // XXX this is running slowly and is too agressive, but it more-or-less works
+        psTrace("psphot", 6, "mask cosmic ray at %f, %f\n", source->peak->xf, source->peak->yf);
+        if (options->apply) {
+            psphotMaskCosmicRay(readout, source, options->crMask);
+        } else {
+            source->mode |= PM_SOURCE_MODE_CR_LIMIT;
+        }
+        nMasked ++;
+
+        // re-subtract the object, leave local sky
+        pmSourceSub (source, PM_MODEL_OP_FULL, options->maskVal);
     }
 
     // now that we have masked pixels associated with CRs, we can grow the mask
-    if (grow > 0) {
+    if (options->grow > 0) {
         bool oldThreads = psImageConvolveSetThreads(true); // Old value of threading for psImageConvolveMask
-        psImage *newMask = psImageConvolveMask(NULL, readout->mask, crMask, crMask, -grow, grow, -grow, grow);
+        psImage *newMask = psImageConvolveMask(NULL, readout->mask, options->crMask, options->crMask, -options->grow, options->grow, -options->grow, options->grow);
         psImageConvolveSetThreads(oldThreads);
         if (!newMask) {
@@ -183,10 +507,481 @@
     }
 
-    psLogMsg ("psphot.size", PS_LOG_INFO, "measure source sizes for %ld sources: %f sec\n",
-              sources->n - first, psTimerMark ("psphot.size"));
-
-    psphotVisualPlotSourceSize (sources);
-    psphotVisualShowSourceSize (readout, sources);
-
+    psLogMsg ("psphot.cr", PS_LOG_INFO, "mask CR: %d masked in %f sec\n", nMasked, psTimerMark ("psphot.cr"));
+
+    // XXX test : save the mask image
+    if (0) {
+        psphotSaveImage (NULL, readout->mask,   "mask.fits");
+    }
+
+    return true;
+}
+
+# define DUMPPICS 0
+# define LIMIT_XRANGE(X, IMAGE) { X = PS_MIN(PS_MAX(0, X), IMAGE->numCols); }
+# define LIMIT_YRANGE(Y, IMAGE) { Y = PS_MIN(PS_MAX(0, Y), IMAGE->numRows); }
+
+// Comments by CZW 20091209 : Mechanics of how to identify CR pixels taken from "Cosmic-Ray
+// Rejection by Laplacian Edge Detection" by Pieter van Dokkum, arXiv:astro-ph/0108003.  This
+// does no repair or recovery of the CR pixels, it only masks them out.  My test code can be
+// found at /data/ipp031.0/watersc1/psphot.20091209/algo_check.c
+bool psphotMaskCosmicRay (pmReadout *readout, pmSource *source, psImageMaskType maskVal) {
+
+    // Get the actual images and information about the peak.
+    psImage *mask = readout->mask;
+    pmPeak *peak = source->peak;
+    pmFootprint *footprint = peak->footprint;
+
+    // Bounding boxes are inclusive of final pixel
+    int xs = footprint->bbox.x0;
+    int xe = footprint->bbox.x1 + 1;
+    int ys = footprint->bbox.y0;
+    int ye = footprint->bbox.y1 + 1;
+
+    LIMIT_XRANGE(xs, mask);
+    LIMIT_XRANGE(xe, mask);
+    LIMIT_YRANGE(ys, mask);
+    LIMIT_YRANGE(ye, mask);
+
+    int dx = xe - xs;
+    int dy = ye - ys;
+
+    psImage *image= readout->image;
+    psImage *variance = readout->variance;
+
+    int binning = 2;
+    float sigma_thresh = 3.0;
+    int max_iter = 1; // XXX with isophot masking, we only want to do a single pass
+
+    // Temporary images.
+    psImage *mypix  = psImageAlloc(dx,dy,image->type.type);
+    psImage *myfix  = psImageAlloc(dx,dy,image->type.type);
+    psImage *myvar  = psImageAlloc(dx,dy,image->type.type);
+    psImage *binned = psImageAlloc(dx * binning,dy * binning,image->type.type);
+    psImage *conved = psImageAlloc(dx * binning,dy * binning,image->type.type);
+    psImage *edges  = psImageAlloc(dx,dy,image->type.type);
+    psImage *mymask = psImageAlloc(dx,dy,PS_TYPE_IMAGE_MASK);
+
+    // Load my copy of things.
+    for (int y = 0; y < dy; y++) {
+        for (int x = 0; x < dx; x++) {
+            mypix->data.F32[y][x] = image->data.F32[y+ys][x+xs];
+            myvar->data.F32[y][x] = variance->data.F32[y+ys][x+xs];
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] = 0x00;
+        }
+    }
+    // Mask so I can see on the output image where the footprint is.
+    for (int i = 0; i < footprint->spans->n; i++) {
+        pmSpan *sp = footprint->spans->data[i];
+        for (int j = sp->x0; j <= sp->x1; j++) {
+            int y = sp->y - ys;
+            int x = j - xs;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] |= 0x01;
+        }
+    }
+
+    int nCRpix = 1; // force at least one pass...
+    for (int iteration = 0; (iteration < max_iter) && (nCRpix > 0); iteration++) {
+        nCRpix = 0;
+        psImageInit (binned, 0.0);
+        psImageInit (conved, 0.0);
+        psImageInit (edges, 0.0);
+
+        // Make subsampled image. Maybe this should be called "unbinned" or something
+        for (int y = 0; y < binning * dy; y++) {
+            int yraw = y / binning;
+            for (int x = 0; x < binning * dx; x++) {
+                int xraw = x / binning;
+                binned->data.F32[y][x] = mypix->data.F32[yraw][xraw];
+            }
+        }
+
+        // Apply Laplace transform (kernel = [[0 -0.25 0][-0.25 1 -0.25][0 -0.25 0]]), clipping at zero
+        for (int y = 1; y < binning * dy - 1; y++) {
+            for (int x = 1; x < binning * dx - 1; x++) {
+                float value = binned->data.F32[y][x] - 0.25 *
+                    (binned->data.F32[y+0][x-1] + binned->data.F32[y+0][x+1] +
+                     binned->data.F32[y-1][x+0] + binned->data.F32[y+1][x+0]);
+                value = PS_MAX(0.0, value);
+
+                conved->data.F32[y][x] = value;
+            }
+        }
+
+        // Create an edge map by rebinning
+        for (int y = 0; y < binning * dy; y++) {
+            int yraw = y / binning;
+            for (int x = 0; x < binning * dx; x++) {
+                int xraw = x / binning;
+                edges->data.F32[yraw][xraw] += conved->data.F32[y][x];
+            }
+        }
+
+        // coordinate of peak in subimage pixels:
+        int xPeak = peak->x - xs;
+        int yPeak = peak->y - ys;
+
+        // Modify my mask if we're above the significance threshold, but only for connected pixels
+        nCRpix = psphotMaskCosmicRayConnected (xPeak, yPeak, mymask, myvar, edges, binning, sigma_thresh);
+
+# if DUMPPICS
+        psphotSaveImage (NULL, mypix,   "crmask.pix.fits");
+# endif
+
+// XXX do not repair the pixels in isophot version
+# if 0
+        // "Repair" Masked pixels for the next round.
+        for (int y = 1; y < dy - 1; y++) {
+            for (int x = 1; x < dx - 1; x++) {
+                if (!(mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & 0x40)) {
+                    myfix->data.F32[y][x] = mypix->data.F32[y][x];
+                    continue;
+                }
+                myfix->data.F32[y][x] = 0.25 *
+                    (mypix->data.F32[y+0][x-1] + mypix->data.F32[y+0][x+1] +
+                     mypix->data.F32[y-1][x+0] + mypix->data.F32[y+1][x+0]);
+            }
+        }
+
+        // "Repair" Masked pixels for the next round.
+        for (int y = 1; y < dy - 1; y++) {
+            for (int x = 1; x < dx - 1; x++) {
+                mypix->data.F32[y][x] = myfix->data.F32[y][x];
+            }
+        }
+# endif
+
+# if DUMPPICS
+        fprintf (stderr, "CRMASK %d %d %d %d %d\n", xs, ys, dx, dy, iteration);
+        psphotSaveImage (NULL, mypix,   "crmask.fix.fits");
+        psphotSaveImage (NULL, myvar,   "crmask.var.fits");
+        psphotSaveImage (NULL, binned,  "crmask.binn.fits");
+        psphotSaveImage (NULL, conved,  "crmask.conv.fits");
+        psphotSaveImage (NULL, edges,   "crmask.edge.fits");
+        psphotSaveImage (NULL, mymask,  "crmask.mask.fits");
+# endif
+        psTrace("psphot.czw",2,"Iter: %d Count: %d",iteration, nCRpix);
+    }
+
+# if 0
+    // A solitary masked pixel is likely a lie. Remove those
+    // XXX can't we use nCRpix == 1 to test for these?
+    for (int x = 0; x < dx; x++) {
+        for (int y = 0; y < dy; y++) {
+            if (!(mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & 0x40)) continue;
+            if ((x-1 >= 0) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x-1] & 0x40)) {
+                continue;
+            }
+            if ((y-1 >= 0) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[y-1][x] & 0x40)) {
+                continue;
+            }
+            if ((x+1 < dx) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x+1] & 0x40)) {
+                continue;
+            }
+            if ((y+1 < dy) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[y+1][x] & 0x40)) {
+                continue;
+            }
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] ^= 0x40;
+        }
+    }
+# endif
+
+    // transfer temporary mask to real mask & count masked pixels
+    nCRpix = 0;
+    for (int x = 0; x < dx; x++) {
+        for (int y = 0; y < dy; y++) {
+            if (mymask->data.PS_TYPE_IMAGE_MASK_DATA[y][x] & 0x40) {
+                mask->data.PS_TYPE_IMAGE_MASK_DATA[y+ys+mask->row0][x+xs+mask->col0] |= maskVal;
+                nCRpix ++;
+            }
+        }
+    }
+
+    // XXX if we decide this REALLY is a cosmic ray, set the CR_LIMIT bit
+    if (nCRpix > 1) {
+        source->mode |= PM_SOURCE_MODE_CR_LIMIT;
+        source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+    }
+    // fprintf (stderr, "CRMASK %d %d %d %d %d\n", peak->x, peak->y, dx, dy, nCRpix);
+
+    psFree(mypix);
+    psFree(myfix);
+    psFree(myvar);
+    psFree(binned);
+    psFree(conved);
+    psFree(edges);
+    psFree(mymask);
+
+    return true;
+}
+
+bool psphotMaskCosmicRayFootprintCheck (psArray *sources) {
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        pmPeak *peak = source->peak;
+        pmFootprint *footprint = peak->footprint;
+        if (!footprint) continue;
+        for (int j = 0; j < footprint->spans->n; j++) {
+            pmSpan *sp = footprint->spans->data[j];
+            psAssert (sp, "missing span");
+        }
+    }
+    return true;
+}
+
+/**** ------ old versions of cosmic ray masking ----- ****/
+
+bool psphotMaskCosmicRayIsophot (pmSource *source, psImageMaskType maskVal, psImageMaskType crMask);
+
+// This attempt to mask the cosmic rays used the isophotal boundary
+bool psphotMaskCosmicRay_V1 (psImage *mask, pmSource *source, psImageMaskType maskVal, psImageMaskType crMask) {
+
+    // replace the source flux
+    pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
+
+    // flag this as a CR
+    source->mode |= PM_SOURCE_MODE_CR_LIMIT;
+    pmPeak *peak = source->peak;
+    psAssert (peak, "NULL peak");
+
+    // grab the matching footprint
+    pmFootprint *footprint = peak->footprint;
+    if (!footprint) {
+      psTrace("psphot.czw",2,"Using isophot CR mask code.");
+
+        // if we have not footprint, use the old code to mask by isophot
+        psphotMaskCosmicRayIsophot (source, maskVal, crMask);
+        return true;
+    }
+
+    if (!footprint->spans) {
+      psTrace("psphot.czw",2,"Using isophot CR mask code.");
+
+        // if we have no footprint, use the old code to mask by isophot
+        psphotMaskCosmicRayIsophot (source, maskVal, crMask);
+        return true;
+    }
+    psphotMaskCosmicRayIsophot (source, maskVal, crMask);
+    // mask all of the pixels covered by the spans of the footprint
+    for (int j = 1; j < footprint->spans->n; j++) {
+        pmSpan *span1 = footprint->spans->data[j];
+
+        int iy = span1->y;
+        int xs = span1->x0;
+        int xe = span1->x1;
+
+        for (int ix = xs; ix < xe; ix++) {
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
+        }
+    }
+    return true;
+}
+
+# define VERBOSE 0
+int psphotMaskCosmicRayConnected (int xPeak, int yPeak, psImage *mymask, psImage *myvar, psImage *edges, int binning, float sigma_thresh) {
+
+    int xLo, xRo;
+    int nCRpix = 0;
+
+    float noise_factor = 5.0 / 4.0;  // Intrinsic to the Laplacian making noise spikes spikier.
+
+    // mark the pixels in this row to the left, then the right. stay within footprint
+    int xL = xPeak; // find the range of valid pixels in this row
+    int xR = xPeak;
+    for (int ix = xPeak; (ix >= 0) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[yPeak][ix] & 0x01); ix--) {
+        float noise = binning * sqrt(noise_factor * myvar->data.F32[yPeak][ix]);
+        float value = edges->data.F32[yPeak][ix] / noise;
+        if (value < sigma_thresh ) break;
+        mymask->data.PS_TYPE_IMAGE_MASK_DATA[yPeak][ix] |= 0x40;
+        xL = ix;
+        nCRpix ++;
+        if (VERBOSE) fprintf (stderr, "mark %d,%d (%d) : %d - %d\n", ix, yPeak, nCRpix, xL, xR);
+    }
+    for (int ix = xPeak; (ix < mymask->numCols) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[yPeak][ix] & 0x01); ix++) {
+        float noise = binning * sqrt(noise_factor * myvar->data.F32[yPeak][ix]);
+        float value = edges->data.F32[yPeak][ix] / noise;
+        if (value < sigma_thresh ) break;
+        mymask->data.PS_TYPE_IMAGE_MASK_DATA[yPeak][ix] |= 0x40;
+        xR = ix;
+        nCRpix ++;
+        if (VERBOSE) fprintf (stderr, "mark %d,%d (%d) : %d - %d\n", ix, yPeak, nCRpix, xL, xR);
+    }
+    // xL and xR mark the first and last valid pixel in the row
+
+    // for each of the neighboring rows, mark the high pixels if they touch the range xL to xR
+    xLo = PS_MAX(xL - 1, 0);
+    xRo = PS_MIN(xR + 1, mymask->numCols);
+
+    // first go down:
+    for (int iy = yPeak - 1; iy >= 0; iy--) {
+
+        int xLn = -1;
+        int xRn = -1;
+        int newPix = 0;
+
+        // mark the pixels in the good range
+        for (int ix = xLo; ix < xRo; ix++) {
+            if (!(mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01)) continue; // only use pixels in the footprint
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) continue;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xLn == -1) xLn = ix; // first valid pixel in this row
+            xRn = ix;                // last valid pixel in this row
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark C %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+
+        // mark the pixels to the left of the good range
+        for (int ix = xLo; (ix >= 0) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01); ix--) {
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) break;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xRn == -1) xRn = ix; // last valid pixel in this row
+            xLn = ix;
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark L %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+
+        // mark the pixels to the right of the good range
+        for (int ix = xRo; (ix < mymask->numCols) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01); ix++) {
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) break;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xLn == -1) xLn = ix; // first valid pixel in this row
+            xRn = ix;
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark R %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+        if (newPix == 0) break;
+        xLo = PS_MAX(xLn - 1, 0);
+        xRo = PS_MIN(xRn + 1, mymask->numCols);
+    }
+
+    xLo = PS_MAX(xL - 1, 0);
+    xRo = PS_MIN(xR + 1, mymask->numCols);
+
+    // next go up:
+    for (int iy = yPeak + 1; iy < mymask->numRows; iy++) {
+
+        int xLn = -1;
+        int xRn = -1;
+        int newPix = 0;
+
+        // mark the pixels in the good range
+        for (int ix = xLo; ix < xRo; ix++) {
+            if (!(mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01)) continue; // only use pixels in the footprint
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) continue;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xLn == -1) xLn = ix; // first valid pixel in this row
+            xRn = ix;                // last valid pixel in this row
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark C %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+
+        // mark the pixels to the left of the good range
+        for (int ix = xLo; (ix >= 0) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01); ix--) {
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) break;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xRn == -1) xRn = ix; // last valid pixel in this row
+            xLn = ix;
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark L %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+
+        // mark the pixels to the right of the good range
+        for (int ix = xRo; (ix < mymask->numCols) && (mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & 0x01); ix++) {
+            float noise = binning * sqrt(noise_factor * myvar->data.F32[iy][ix]);
+            float value = edges->data.F32[iy][ix] / noise;
+            if (value < sigma_thresh ) break;
+            mymask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= 0x40;
+            if (xLn == -1) xLn = ix; // first valid pixel in this row
+            xRn = ix;
+            nCRpix ++;
+            newPix ++;
+            if (VERBOSE) fprintf (stderr, "mark R %d,%d (%d) : %d - %d | %d - %d | %d - %d \n", ix, iy, nCRpix, xL, xR, xLo, xRo, xLn, xRn);
+        }
+        if (newPix == 0) break;
+        xLo = PS_MAX(xLn - 1, 0);
+        xRo = PS_MIN(xRn + 1, mymask->numCols);
+    }
+
+    return nCRpix;
+}
+
+bool psphotMaskCosmicRayIsophot (pmSource *source, psImageMaskType maskVal, psImageMaskType crMask) {
+
+    source->mode |= PM_SOURCE_MODE_CR_LIMIT;
+    pmPeak *peak = source->peak;
+    psAssert (peak, "NULL peak");
+
+    psImage *mask   = source->maskView;
+    psImage *pixels = source->pixels;
+    psImage *variance = source->variance;
+
+    // XXX This should be a recipe variable
+# define SN_LIMIT 5.0
+
+    int xo = peak->x - pixels->col0;
+    int yo = peak->y - pixels->row0;
+
+    // mark the pixels in this row to the left, then the right
+    for (int ix = xo; ix >= 0; ix--) {
+        float SN = pixels->data.F32[yo][ix] / sqrt(variance->data.F32[yo][ix]);
+        if (SN > SN_LIMIT) {
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[yo][ix] |= crMask;
+        }
+    }
+    for (int ix = xo + 1; ix < pixels->numCols; ix++) {
+        float SN = pixels->data.F32[yo][ix] / sqrt(variance->data.F32[yo][ix]);
+        if (SN > SN_LIMIT) {
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[yo][ix] |= crMask;
+        }
+    }
+
+    // for each of the neighboring rows, mark the high pixels if they have a marked neighbor
+    // first go up:
+    for (int iy = PS_MIN(yo, mask->numRows-2); iy >= 0; iy--) {
+        // mark the pixels in this row to the left, then the right
+        for (int ix = 0; ix < pixels->numCols; ix++) {
+            float SN = pixels->data.F32[iy][ix] / sqrt(variance->data.F32[iy][ix]);
+            if (SN < SN_LIMIT) continue;
+
+            bool valid = false;
+            valid |= (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix] & crMask);
+            valid |= (ix > 0) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix-1] & crMask) : 0;
+            valid |= (ix <= mask->numCols) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix+1] & crMask) : 0;
+
+            if (!valid) continue;
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
+        }
+    }
+    // next go down:
+    for (int iy = PS_MIN(yo+1, mask->numRows-1); iy < pixels->numRows; iy++) {
+        // mark the pixels in this row to the left, then the right
+        for (int ix = 0; ix < pixels->numCols; ix++) {
+            float SN = pixels->data.F32[iy][ix] / sqrt(variance->data.F32[iy][ix]);
+            if (SN < SN_LIMIT) continue;
+
+            bool valid = false;
+            valid |= (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix] & crMask);
+            valid |= (ix > 0) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix-1] & crMask) : 0;
+            valid |= (ix <= mask->numCols) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix+1] & crMask) : 0;
+
+            if (!valid) continue;
+            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
+        }
+    }
     return true;
 }
@@ -194,7 +989,8 @@
 // given the PSF ellipse parameters, navigate around the 1sigma contour, return the total
 // deviation in sigmas.  This is measured on the residual image - should we ignore negative
-// deviations?
-static float psphotModelContour(const psImage *image, const psImage *variance, const psImage *mask,
-                                psImageMaskType maskVal, const pmModel *model, float Ro)
+// deviations?  NOTE: This function was an early attempt to classify extended objects, and is
+// no longer used by psphot.
+float psphotModelContour(const psImage *image, const psImage *variance, const psImage *mask,
+                         psImageMaskType maskVal, const pmModel *model, float Ro)
 {
     psF32 *PAR = model->params->data.F32; // Model parameters
@@ -265,107 +1061,136 @@
 }
 
-bool psphotMaskCosmicRay_New (psImage *mask, pmSource *source, psImageMaskType maskVal, psImageMaskType crMask) {
-
-    // replace the source flux
-    pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
-    source->tmpFlags &= ~PM_SOURCE_TMPF_SUBTRACTED;
-
-    // flag this as a CR
-    source->mode |= PM_SOURCE_MODE_CR_LIMIT;
-    pmPeak *peak = source->peak;
-    psAssert (peak, "NULL peak");
-
-    // grab the matching footprint
-    pmFootprint *footprint = peak->footprint;
-    if (!footprint) {
-        // if we have not footprint, use the old code to mask by isophot
-        psphotMaskCosmicRay_Old (source, maskVal, crMask);
-        return true;
-    }
-
-    if (!footprint->spans) {
-        // if we have not footprint, use the old code to mask by isophot
-        psphotMaskCosmicRay_Old (source, maskVal, crMask);
-        return true;
-    }
-
-    // mask all of the pixels covered by the spans of the footprint
-    for (int j = 1; j < footprint->spans->n; j++) {
-        pmSpan *span1 = footprint->spans->data[j];
-
-        int iy = span1->y;
-        int xs = span1->x0;
-        int xe = span1->x1;
-
-        for (int ix = xs; ix < xe; ix++) {
-            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
-        }
+// this was an old attempt to identify cosmic rays based on the peak curvature
+bool psphotSourcePeakCurvature (pmReadout *readout, psArray *sources, psphotSourceSizeOptions *options) {
+
+    // classify the sources based on the CR test (place this in a function?)
+    // XXX use an internal flag to mark sources which have already been measured
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        // skip source if it was already measured
+        if (source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED) {
+            psTrace("psphot", 7, "Not calculating source size since it has already been measured\n");
+            continue;
+        }
+
+        // source must have been subtracted
+        if (!(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED)) {
+            source->mode |= PM_SOURCE_MODE_SIZE_SKIPPED;
+            psTrace("psphot", 7, "Not calculating source size since source is not subtracted\n");
+            continue;
+        }
+
+        psF32 **resid  = source->pixels->data.F32;
+        psF32 **variance = source->variance->data.F32;
+        psImageMaskType **mask = source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA;
+
+        // Integer position of peak
+        int xPeak = source->peak->xf - source->pixels->col0 + 0.5;
+        int yPeak = source->peak->yf - source->pixels->row0 + 0.5;
+
+        // Skip sources which are too close to a boundary.  These are mostly caught as DEFECT
+        if (xPeak < 1 || xPeak > source->pixels->numCols - 2 ||
+            yPeak < 1 || yPeak > source->pixels->numRows - 2) {
+            psTrace("psphot", 7, "Not calculating crNsigma due to edge\n");
+            continue;
+        }
+
+        // Skip sources with masked pixels.  These are mostly caught as DEFECT
+        bool keep = true;
+        for (int iy = -1; (iy <= +1) && keep; iy++) {
+            for (int ix = -1; (ix <= +1) && keep; ix++) {
+                if (mask[yPeak+iy][xPeak+ix] & options->maskVal) {
+                    keep = false;
+                }
+            }
+        }
+        if (!keep) {
+            psTrace("psphot", 7, "Not calculating crNsigma due to masked pixels\n");
+            continue;
+        }
+
+        // Compare the central pixel with those on either side, for the four possible lines through it.
+
+        // Soften variances (add systematic error)
+        float softening = options->soft * PS_SQR(source->peak->flux); // Softening for variances
+
+        // Across the middle: y = 0
+        float cX = 2*resid[yPeak][xPeak]   - resid[yPeak+0][xPeak-1]  - resid[yPeak+0][xPeak+1];
+        float dcX = 4*variance[yPeak][xPeak] + variance[yPeak+0][xPeak-1] + variance[yPeak+0][xPeak+1];
+        float nX = cX / sqrtf(dcX + softening);
+
+        // Up the centre: x = 0
+        float cY = 2*resid[yPeak][xPeak]   - resid[yPeak-1][xPeak+0]  - resid[yPeak+1][xPeak+0];
+        float dcY = 4*variance[yPeak][xPeak] + variance[yPeak-1][xPeak+0] + variance[yPeak+1][xPeak+0];
+        float nY = cY / sqrtf(dcY + softening);
+
+        // Diagonal: x = y
+        float cL = 2*resid[yPeak][xPeak]   - resid[yPeak-1][xPeak-1]  - resid[yPeak+1][xPeak+1];
+        float dcL = 4*variance[yPeak][xPeak] + variance[yPeak-1][xPeak-1] + variance[yPeak+1][xPeak+1];
+        float nL = cL / sqrtf(dcL + softening);
+
+        // Diagonal: x = - y
+        float cR = 2*resid[yPeak][xPeak]   - resid[yPeak+1][xPeak-1]  - resid[yPeak-1][xPeak+1];
+        float dcR = 4*variance[yPeak][xPeak] + variance[yPeak+1][xPeak-1] + variance[yPeak-1][xPeak+1];
+        float nR = cR / sqrtf(dcR + softening);
+
+        // P(chisq > chisq_obs; Ndof) = gamma_Q (Ndof/2, chisq/2)
+        // Ndof = 4 ? (four measurements, no free parameters)
+        // XXX this value is going to be biased low because of systematic errors.
+        // we need to calibrate it somehow
+        // source->psfProb = gsl_sf_gamma_inc_Q (2, 0.5*chisq);
+
+        // not strictly accurate: overcounts the chisq contribution from the center pixel (by
+        // factor of 4); also biases a bit low if any pixels are masked
+        // XXX I am not sure I want to keep this value...
+        source->psfChisq = PS_SQR(nX) + PS_SQR(nY) + PS_SQR(nL) + PS_SQR(nR);
+
+        float fCR = 0.0;
+        int nCR = 0;
+        if (nX > 0.0) {
+            fCR += nX;
+            nCR ++;
+        }
+        if (nY > 0.0) {
+            fCR += nY;
+            nCR ++;
+        }
+        if (nL > 0.0) {
+            fCR += nL;
+            nCR ++;
+        }
+        if (nR > 0.0) {
+            fCR += nR;
+            nCR ++;
+        }
+        source->crNsigma  = (nCR > 0)  ? fCR / nCR : 0.0;
+        source->tmpFlags |= PM_SOURCE_TMPF_SIZE_MEASURED;
+
+        if (!isfinite(source->crNsigma)) {
+            continue;
+        }
+
+        // this source is thought to be a cosmic ray.  flag the detection and mask the pixels
+        if (source->crNsigma > options->nSigmaCR) {
+            source->mode |= PM_SOURCE_MODE_CR_LIMIT;
+            // XXX still testing... : psphotMaskCosmicRay (readout->mask, source, maskVal, crMask);
+            // XXX acting strange... psphotMaskCosmicRay_Old (source, maskVal, crMask);
+        }
+    }
+
+    // now that we have masked pixels associated with CRs, we can grow the mask
+    if (options->grow > 0) {
+        bool oldThreads = psImageConvolveSetThreads(true); // Old value of threading for psImageConvolveMask
+        psImage *newMask = psImageConvolveMask(NULL, readout->mask, options->crMask, options->crMask, -options->grow, options->grow, -options->grow, options->grow);
+        psImageConvolveSetThreads(oldThreads);
+        if (!newMask) {
+            psError(PS_ERR_UNKNOWN, false, "Unable to grow CR mask");
+            return false;
+        }
+        psFree(readout->mask);
+        readout->mask = newMask;
     }
     return true;
 }
 
-bool psphotMaskCosmicRay_Old (pmSource *source, psImageMaskType maskVal, psImageMaskType crMask) {
-
-    source->mode |= PM_SOURCE_MODE_CR_LIMIT;
-    pmPeak *peak = source->peak;
-    psAssert (peak, "NULL peak");
-
-    psImage *mask   = source->maskView;
-    psImage *pixels = source->pixels;
-    psImage *variance = source->variance;
-
-    // XXX This should be a recipe variable
-# define SN_LIMIT 5.0
-
-    int xo = peak->x - pixels->col0;
-    int yo = peak->y - pixels->row0;
-
-    // mark the pixels in this row to the left, then the right
-    for (int ix = xo; ix >= 0; ix--) {
-        float SN = pixels->data.F32[yo][ix] / sqrt(variance->data.F32[yo][ix]);
-        if (SN > SN_LIMIT) {
-            mask->data.PS_TYPE_IMAGE_MASK_DATA[yo][ix] |= crMask;
-        }
-    }
-    for (int ix = xo + 1; ix < pixels->numCols; ix++) {
-        float SN = pixels->data.F32[yo][ix] / sqrt(variance->data.F32[yo][ix]);
-        if (SN > SN_LIMIT) {
-            mask->data.PS_TYPE_IMAGE_MASK_DATA[yo][ix] |= crMask;
-        }
-    }
-
-    // for each of the neighboring rows, mark the high pixels if they have a marked neighbor
-    // first go up:
-    for (int iy = PS_MIN(yo, mask->numRows-2); iy >= 0; iy--) {
-        // mark the pixels in this row to the left, then the right
-        for (int ix = 0; ix < pixels->numCols; ix++) {
-            float SN = pixels->data.F32[iy][ix] / sqrt(variance->data.F32[iy][ix]);
-            if (SN < SN_LIMIT) continue;
-
-            bool valid = false;
-            valid |= (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix] & crMask);
-            valid |= (ix > 0) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix-1] & crMask) : 0;
-            valid |= (ix <= mask->numCols) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy+1][ix+1] & crMask) : 0;
-
-            if (!valid) continue;
-            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
-        }
-    }
-    // next go down:
-    for (int iy = PS_MIN(yo+1, mask->numRows-1); iy < pixels->numRows; iy++) {
-        // mark the pixels in this row to the left, then the right
-        for (int ix = 0; ix < pixels->numCols; ix++) {
-            float SN = pixels->data.F32[iy][ix] / sqrt(variance->data.F32[iy][ix]);
-            if (SN < SN_LIMIT) continue;
-
-            bool valid = false;
-            valid |= (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix] & crMask);
-            valid |= (ix > 0) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix-1] & crMask) : 0;
-            valid |= (ix <= mask->numCols) ? (mask->data.PS_TYPE_IMAGE_MASK_DATA[iy-1][ix+1] & crMask) : 0;
-
-            if (!valid) continue;
-            mask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] |= crMask;
-        }
-    }
-    return true;
-}
Index: branches/simtest_nebulous_branches/psphot/src/psphotSourceStats.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSourceStats.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSourceStats.c	(revision 27840)
@@ -1,5 +1,30 @@
 # include "psphotInternal.h"
 
-psArray *psphotSourceStats (pmConfig *config, pmReadout *readout, pmDetections *detections) {
+// convert detections to sources and measure their basic properties (moments, local sky, sky
+// variance) Note: this function only generates sources for the new peaks (peak->assigned).
+// The new sources are added to any existing sources on detections->newSources.  The sources 
+// on detections->allSources are ignored.
+bool psphotSourceStats (pmConfig *config, const pmFPAview *view, bool setWindow)
+{
+    bool status = true;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (!psphotSourceStatsReadout (config, view, "PSPHOT.INPUT", i, recipe, setWindow)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to find initial detections for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+    return true;
+}
+
+bool psphotSourceStatsReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe, bool setWindow) {
 
     bool status = false;
@@ -8,7 +33,21 @@
     psTimerStart ("psphot.stats");
 
-    // select the appropriate recipe information
-    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
-    assert (recipe);
+    // find the currently selected readout
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (file, "missing file?");
+
+    pmReadout *readout = pmFPAviewThisReadout(view, file->fpa);
+    psAssert (readout, "missing readout?");
+
+    pmDetections *detections = psMetadataLookupPtr (&status, readout->analysis, "PSPHOT.DETECTIONS");
+    psAssert (detections, "missing detections?");
+
+    // XXX TEST:
+    if (detections->allSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->allSources);
+    }
+    if (detections->newSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->newSources);
+    }
 
     // determine the number of allowed threads
@@ -19,17 +58,40 @@
 
     // determine properties (sky, moments) of initial sources
-    float OUTER    = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
-    if (!status) return NULL;
+    float OUTER = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    psAssert (status, "missing SKY_OUTER_RADIUS in recipe?");
+
+    // XXX this seems like an arbitrary number...
+    OUTER = PS_MAX(OUTER, 20.0); // XXX Guarantee that we can encompass the max moments radius
+
     char *breakPt  = psMetadataLookupStr (&status, recipe, "BREAK_POINT");
-    if (!status) return NULL;
+    psAssert (status, "missing BREAK_POINT?");
+
+    float INNER        = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS"); psAssert (status, "missing SKY_INNER_RADIUS");
+    float MIN_SN       = psMetadataLookupF32 (&status, recipe, "MOMENTS_SN_MIN"); psAssert (status, "missing MOMENTS_SN_MIN");
+
+    // bit-masks to test for good/bad pixels
+    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT");
+    psAssert (maskVal, "missing MASK.PSPHOT");
+
+    // bit-mask to mark pixels not used in analysis
+    psImageMaskType markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT");
+    psAssert (markVal, "missing MARK.PSPHOT");
 
     psArray *peaks = detections->peaks;
     if (!peaks) {
         psError(PS_ERR_UNEXPECTED_NULL, false, "No peaks found!");
-        return NULL;
+        return false;
     }
 
     // generate the array of sources, define the associated pixel
-    sources = psArrayAllocEmpty (peaks->n);
+    if (!detections->newSources) {
+	detections->newSources = psArrayAllocEmpty (peaks->n);
+    }
+    sources = detections->newSources;
+
+    // if there are no peaks, we save the empty source array and return
+    if (!peaks->n) {
+        return true;
+    }
 
     for (int i = 0; i < peaks->n; i++) {
@@ -40,4 +102,5 @@
         // create a new source
         pmSource *source = pmSourceAlloc();
+	source->imageID = index;
 
         // add the peak
@@ -53,4 +116,154 @@
         psArrayAdd (sources, 100, source);
         psFree (source);
+    }
+
+    if (!strcasecmp (breakPt, "PEAKS")) {
+        psLogMsg ("psphot", PS_LOG_INFO, "%ld sources : %f sec\n", sources->n, psTimerMark ("psphot.stats"));
+        psLogMsg ("psphot", PS_LOG_INFO, "break point PEAKS, skipping MOMENTS\n");
+        psphotVisualShowMoments (sources);
+        return true;
+    }
+
+    if (setWindow) {
+        if (!psphotSetMomentsWindow(recipe, readout->analysis, sources)) {
+            psError(PS_ERR_UNEXPECTED_NULL, false, "Failed to determine Moments Window!");
+            psFree(detections->newSources);
+            return false;
+        }
+    }
+
+    // if we have measured the window, we will be saving the modified version of these recipe values on readout->analysis
+    float SIGMA = psMetadataLookupF32 (&status, readout->analysis, "MOMENTS_GAUSS_SIGMA");
+    if (!status) {
+        SIGMA = psMetadataLookupF32 (&status, recipe, "MOMENTS_GAUSS_SIGMA");
+    }
+    float RADIUS = psMetadataLookupF32 (&status, readout->analysis, "PSF_MOMENTS_RADIUS");
+    if (!status) {
+        RADIUS = psMetadataLookupF32 (&status, recipe, "PSF_MOMENTS_RADIUS");
+    }
+
+    // threaded measurement of the source magnitudes
+    int Nfail = 0;
+    int Nmoments = 0;
+    int Nfaint = 0;
+
+    // choose Cx, Cy (see psphotThreadTools.c for overview of the concepts)
+    int Cx = 1, Cy = 1;
+    psphotChooseCellSizes (&Cx, &Cy, readout, nThreads);
+
+    psArray *cellGroups = psphotAssignSources (Cx, Cy, sources);
+
+    for (int i = 0; i < cellGroups->n; i++) {
+
+        psArray *cells = cellGroups->data[i];
+
+        for (int j = 0; j < cells->n; j++) {
+
+            // allocate a job -- if threads are not defined, this just runs the job
+            psThreadJob *job = psThreadJobAlloc ("PSPHOT_SOURCE_STATS");
+
+            psArrayAdd(job->args, 1, cells->data[j]); // sources
+
+            PS_ARRAY_ADD_SCALAR(job->args, INNER,   PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, MIN_SN,  PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, RADIUS,  PS_TYPE_F32);
+            PS_ARRAY_ADD_SCALAR(job->args, SIGMA,   PS_TYPE_F32);
+
+            PS_ARRAY_ADD_SCALAR(job->args, maskVal, PS_TYPE_IMAGE_MASK);
+            PS_ARRAY_ADD_SCALAR(job->args, markVal, PS_TYPE_IMAGE_MASK);
+
+            PS_ARRAY_ADD_SCALAR(job->args, 0, PS_TYPE_S32); // this is used as a return value for Nmoments
+            PS_ARRAY_ADD_SCALAR(job->args, 0, PS_TYPE_S32); // this is used as a return value for Nfail
+            PS_ARRAY_ADD_SCALAR(job->args, 0, PS_TYPE_S32); // this is used as a return value for Nfaint
+
+            if (!psThreadJobAddPending(job)) {
+                psError(PS_ERR_UNKNOWN, false, "Unable to launch thread job PSPHOT_SOURCE_STATS");
+                psFree (job);
+                psFree(detections->newSources);
+                return false;
+            }
+            psFree(job);
+        }
+
+        // wait for the threads to finish and manage results
+        if (!psThreadPoolWait (false)) {
+            psError(PS_ERR_UNKNOWN, false, "Failure in thread job PSPHOT_SOURCE_STATS");
+	    psFree(detections->newSources);
+            return false;
+        }
+
+        // we have only supplied one type of job, so we can assume the types here
+        psThreadJob *job = NULL;
+        while ((job = psThreadJobGetDone()) != NULL) {
+            if (job->args->n < 1) {
+                fprintf (stderr, "error with job\n");
+            } else {
+                psScalar *scalar = NULL;
+                scalar = job->args->data[7];
+                Nmoments += scalar->data.S32;
+                scalar = job->args->data[8];
+                Nfail += scalar->data.S32;
+                scalar = job->args->data[9];
+                Nfaint += scalar->data.S32;
+            }
+            psFree(job);
+        }
+    }
+
+    psFree (cellGroups);
+
+    psLogMsg ("psphot", PS_LOG_INFO, "%ld sources, %d moments, %d faint, %d failed: %f sec\n", sources->n, Nmoments, Nfaint, Nfail, psTimerMark ("psphot.stats"));
+
+    psphotVisualShowMoments (sources);
+
+    if (detections->allSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->allSources);
+    }
+    if (detections->newSources) {
+        psphotMaskCosmicRayFootprintCheck(detections->newSources);
+    }
+
+    return true;
+}
+
+// this function is currently only called by psphotCheckExtSources
+bool psphotSourceStatsUpdate (psArray *sources, pmConfig *config, pmReadout *readout) {
+
+    bool status = false;
+
+    psTimerStart ("psphot.stats");
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    assert (recipe);
+
+    // determine the number of allowed threads
+    int nThreads = psMetadataLookupS32(&status, config->arguments, "NTHREADS"); // Number of threads
+    if (!status) {
+        nThreads = 0;
+    }
+
+    // determine properties (sky, moments) of initial sources
+    float OUTER    = psMetadataLookupF32 (&status, recipe, "SKY_OUTER_RADIUS");
+    if (!status) return false;
+
+    OUTER = PS_MAX(OUTER, 20.0); // XXX Guarantee that we can encompass the max moments radius
+
+    char *breakPt  = psMetadataLookupStr (&status, recipe, "BREAK_POINT");
+    if (!status) return false;
+
+    for (int i = 0; i < sources->n; i++) {
+
+        pmSource *source = sources->data[i];
+        if (!source->peak) continue; // XXX how can we have a peak-less source?
+
+        // allocate space for moments
+        if (!source->moments) {
+            source->moments = pmMomentsAlloc();
+        }
+
+        // allocate image, weight, mask arrays for each peak (square of radius OUTER)
+        pmSourceDefinePixels (source, readout, source->peak->x, source->peak->y, OUTER);
+        source->peak->assigned = true;
     }
 
@@ -62,4 +275,10 @@
     }
 
+    // XXX how else could we get the window size in?
+    if (!psphotSetMomentsWindow(recipe, readout->analysis, sources)) {
+        psError(PS_ERR_UNEXPECTED_NULL, false, "Failed to determine Moments Window!");
+        return NULL;
+    }
+
     // threaded measurement of the source magnitudes
     int Nfail = 0;
@@ -82,4 +301,5 @@
             psThreadJob *job = psThreadJobAlloc ("PSPHOT_SOURCE_STATS");
 
+            // XXX: this must match the above
             psArrayAdd(job->args, 1, cells->data[j]); // sources
             psArrayAdd(job->args, 1, recipe);
@@ -136,24 +356,12 @@
 
     psArray *sources                = job->args->data[0];
-    psMetadata *recipe              = job->args->data[1];
-
-    float INNER        = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS");
-    if (!status) return false;
-    float MIN_SN       = psMetadataLookupF32 (&status, recipe, "MOMENTS_SN_MIN");
-    if (!status) return false;
-    float RADIUS       = psMetadataLookupF32 (&status, recipe, "PSF_MOMENTS_RADIUS");
-    if (!status) return false;
-    float MIN_PIXEL_SN = psMetadataLookupF32 (&status, recipe, "MOMENTS_MIN_PIXEL_SN");
-    if (!status) return false;
-    float SIGMA        = psMetadataLookupF32 (&status, recipe, "MOMENTS_GAUSS_SIGMA");
-    if (!status) return false;
-
-    // bit-masks to test for good/bad pixels
-    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT");
-    assert (maskVal);
-
-    // bit-mask to mark pixels not used in analysis
-    psImageMaskType markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT");
-    assert (markVal);
+
+    float INNER                     = PS_SCALAR_VALUE(job->args->data[1],F32);
+    float MIN_SN                    = PS_SCALAR_VALUE(job->args->data[2],F32);
+    float RADIUS                    = PS_SCALAR_VALUE(job->args->data[3],F32);
+    float SIGMA                     = PS_SCALAR_VALUE(job->args->data[4],F32);
+
+    psImageMaskType maskVal         = PS_SCALAR_VALUE(job->args->data[5],PS_TYPE_IMAGE_MASK_DATA);
+    psImageMaskType markVal         = PS_SCALAR_VALUE(job->args->data[6],PS_TYPE_IMAGE_MASK_DATA);
 
     // maskVal is used to test for rejected pixels, and must include markVal
@@ -166,4 +374,7 @@
     for (int i = 0; i < sources->n; i++) {
         pmSource *source = sources->data[i];
+
+	if (source->tmpFlags & PM_SOURCE_TMPF_MOMENTS_MEASURED) continue;
+	source->tmpFlags |= PM_SOURCE_TMPF_MOMENTS_MEASURED;
 
         // skip faint sources for moments measurement
@@ -194,6 +405,6 @@
         }
 
-        // measure basic source moments
-        status = pmSourceMoments (source, RADIUS, SIGMA, MIN_PIXEL_SN);
+        // measure basic source moments (no S/N clipping on input pixels)
+        status = pmSourceMoments (source, RADIUS, SIGMA, 0.0, maskVal);
         if (status) {
             Nmoments ++;
@@ -205,5 +416,5 @@
         BIG_RADIUS = PS_MIN (INNER, 3*RADIUS);
         psTrace ("psphot", 4, "retrying moments for %d, %d\n", source->peak->x, source->peak->y);
-        status = pmSourceMoments (source, BIG_RADIUS, 3.0*SIGMA, MIN_PIXEL_SN);
+        status = pmSourceMoments (source, BIG_RADIUS, 3.0*SIGMA, 0.0, maskVal);
         if (status) {
             source->mode |= PM_SOURCE_MODE_BIG_RADIUS;
@@ -219,11 +430,11 @@
 
     // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    scalar = job->args->data[2];
+    scalar = job->args->data[7];
     scalar->data.S32 = Nmoments;
 
-    scalar = job->args->data[3];
+    scalar = job->args->data[8];
     scalar->data.S32 = Nfail;
 
-    scalar = job->args->data[4];
+    scalar = job->args->data[9];
     scalar->data.S32 = Nfaint;
 
@@ -231,84 +442,98 @@
 }
 
-# if (0)
-bool psphotSourceStats_Unthreaded (int *nfail, int *nmoments, psArray *sources, psMetadata *recipe) {
-
-    bool status = false;
-    float BIG_RADIUS;
-
-    float INNER    = psMetadataLookupF32 (&status, recipe, "SKY_INNER_RADIUS");
-    if (!status) return false;
-    float MIN_SN   = psMetadataLookupF32 (&status, recipe, "MOMENTS_SN_MIN");
-    if (!status) return false;
-    float RADIUS   = psMetadataLookupF32 (&status, recipe, "PSF_MOMENTS_RADIUS");
-    if (!status) return false;
-
-    // bit-masks to test for good/bad pixels
-    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT");
-    assert (maskVal);
-
-    // bit-mask to mark pixels not used in analysis
-    psImageMaskType markVal = psMetadataLookupImageMask(&status, recipe, "MARK.PSPHOT");
-    assert (markVal);
-
-    // maskVal is used to test for rejected pixels, and must include markVal
-    maskVal |= markVal;
-
-    // threaded measurement of the sources moments
-    int Nfail = 0;
-    int Nmoments = 0;
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-
-        // skip faint sources for moments measurement
-        if (source->peak->SN < MIN_SN) {
-            continue;
-        }
-
-        // measure a local sky value
-        // the local sky is now ignored; kept here for reference only
-        status = pmSourceLocalSky (source, PS_STAT_SAMPLE_MEDIAN, INNER, maskVal, markVal);
-        if (!status) {
-            psErrorClear(); // XXX re-consider the errors raised here
-            Nfail ++;
-            continue;
-        }
-
-        // measure the local sky variance (needed if noise is not sqrt(signal))
-        // XXX EAM : this should use ROBUST not SAMPLE median, but it is broken
-        status = pmSourceLocalSkyVariance (source, PS_STAT_SAMPLE_MEDIAN, INNER, maskVal, markVal);
-        if (!status) {
-            Nfail ++;
-            psErrorClear();
-            continue;
-        }
-
-        // measure basic source moments
-        status = pmSourceMoments (source, RADIUS, SIGMA, MIN_PIXEL_SN);
-        if (status) {
-            Nmoments ++;
-            continue;
-        }
-
-        // if no valid pixels, or massive swing, likely saturated source,
-        // try a much larger box
-        BIG_RADIUS = PS_MIN (INNER, 3*RADIUS);
-        psTrace ("psphot", 4, "retrying moments for %d, %d\n", source->peak->x, source->peak->y);
-        status = pmSourceMoments (source, BIG_RADIUS, 3.0*SIGMA, MIN_PIXEL_SN);
-        if (status) {
-            Nmoments ++;
-            continue;
-        }
-
-        Nfail ++;
-        psErrorClear();
-        continue;
-    }
-
-    // change the value of a scalar on the array (wrap this and put it in psArray.h)
-    *nmoments = Nmoments;
-    *nfail = Nfail;
-
+// this function attempts to iteratively determine the best value for sigma of the moments weighting Gaussian
+// this function modifies the recipe values MOMENTS_SX_MAX, MOMENTS_SY_MAX, and PSF_CLUMP_GRID_SCALE, used by pmSourcePSFClump
+bool psphotSetMomentsWindow (psMetadata *recipe, psMetadata *analysis, psArray *sources) {
+
+    bool status;
+
+    float MIN_SN = psMetadataLookupF32 (&status, recipe, "MOMENTS_SN_MIN"); psAssert (status, "missing MOMENTS_SN_MIN");
+    float PSF_SN_LIM = psMetadataLookupF32(&status, recipe, "PSF_SN_LIM"); psAssert (status, "missing PSF_SN_LIM");
+    psF32 MOMENTS_AR_MAX = psMetadataLookupF32(&status, recipe, "MOMENTS_AR_MAX"); psAssert (status, "missing MOMENTS_AR_MAX");
+
+    // XXX move this to a config file?
+    # define NSIGMA 8
+    float sigma[NSIGMA] = {1.0, 2.0, 3.0, 4.5, 6.0, 9.0, 12.0, 18.0};
+    float Sout[NSIGMA];
+
+    // this sorts by peak->SN
+    sources = psArraySort (sources, pmSourceSortBySN);
+
+    // loop over radii:
+    for (int i = 0; i < NSIGMA; i++) {
+
+        // XXX move max source number to config
+        for (int j = 0; (j < sources->n) && (j < 400); j++) {
+
+            pmSource *source = sources->data[j];
+            psAssert (source->moments, "force moments to exist");
+            source->moments->nPixels = 0;
+
+            // skip faint sources for moments measurement
+            if (source->peak->SN < MIN_SN) {
+                continue;
+            }
+
+            // measure basic source moments (no S/N clipping on input pixels)
+            status = pmSourceMoments (source, 4*sigma[i], sigma[i], 0.0, 0xffff);
+        }
+
+        // choose a grid scale that is a fixed fraction of the psf sigma^2
+        float PSF_CLUMP_GRID_SCALE = 0.1*PS_SQR(sigma[i]);
+        float MOMENTS_SX_MAX = 2.0*PS_SQR(sigma[i]);
+        float MOMENTS_SY_MAX = 2.0*PS_SQR(sigma[i]);
+
+        // determine the PSF parameters from the source moment values
+        pmPSFClump psfClump = pmSourcePSFClump (NULL, NULL, sources, PSF_SN_LIM, PSF_CLUMP_GRID_SCALE, MOMENTS_SX_MAX, MOMENTS_SY_MAX, MOMENTS_AR_MAX);
+        psLogMsg ("psphot", 3, "radius %.1f, nStars: %d, nSigma: %5.2f, X,  Y: %f, %f (%f, %f)\n", sigma[i], psfClump.nStars, psfClump.nSigma, psfClump.X, psfClump.Y, sqrt(psfClump.X) / sigma[i], sqrt(psfClump.Y) / sigma[i]);
+
+        psMetadataAddS32 (analysis, PS_LIST_TAIL, "PSF.CLUMP.NREGIONS",  PS_META_REPLACE, "psf clump regions", 1);
+        psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, "PSF.CLUMP.REGION.000");
+        if (!regionMD) {
+            regionMD = psMetadataAlloc();
+            psMetadataAddMetadata (analysis, PS_LIST_TAIL, "PSF.CLUMP.REGION.000", PS_META_REPLACE, "psf clump region", regionMD);
+            psFree (regionMD);
+        }
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.X",  PS_META_REPLACE, "psf clump center", psfClump.X);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.Y",  PS_META_REPLACE, "psf clump center", psfClump.Y);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DX", PS_META_REPLACE, "psf clump center", psfClump.dX);
+        psMetadataAddF32 (regionMD, PS_LIST_TAIL, "PSF.CLUMP.DY", PS_META_REPLACE, "psf clump center", psfClump.dY);
+
+        psphotVisualPlotMoments (recipe, analysis, sources);
+
+        Sout[i] = sqrt(0.5*(psfClump.X + psfClump.Y)) / sigma[i];
+    }
+
+    // we are looking for sigma for which Sout = 0.65 (or some other value)
+    float Sigma = NAN;
+    float minS = Sout[0];
+    float maxS = Sout[0];
+    for (int i = 0; i < NSIGMA; i++) {
+        minS = PS_MIN(Sout[i], minS);
+        maxS = PS_MAX(Sout[i], maxS);
+    }
+    if (minS > 0.65) Sigma = sigma[NSIGMA-1];
+    if (maxS < 0.65) Sigma = sigma[0];
+
+    for (int i = 0; i < NSIGMA - 1 && isnan(Sigma); i++) {
+        if (!isfinite(Sout[i]) || !isfinite(Sout[i+1])) continue;
+        if ((Sout[i] > 0.65) && (Sout[i+1] > 0.65)) continue;
+        if ((Sout[i] < 0.65) && (Sout[i+1] < 0.65)) continue;
+        Sigma = sigma[i] + (0.65 - Sout[i])*(sigma[i+1] - sigma[i])/(Sout[i+1] - Sout[i]);
+    }
+    psAssert (isfinite(Sigma), "did we miss a case?");
+
+    // choose a grid scale that is a fixed fraction of the psf sigma^2
+    psMetadataAddF32(analysis, PS_LIST_TAIL, "PSF_CLUMP_GRID_SCALE", PS_META_REPLACE, "clump grid", 0.1*PS_SQR(Sigma));
+    psMetadataAddF32(analysis, PS_LIST_TAIL, "MOMENTS_SX_MAX", PS_META_REPLACE, "moments limit", 2.0*PS_SQR(Sigma));
+    psMetadataAddF32(analysis, PS_LIST_TAIL, "MOMENTS_SY_MAX", PS_META_REPLACE, "moments limit", 2.0*PS_SQR(Sigma));
+    psMetadataAddF32(analysis, PS_LIST_TAIL, "MOMENTS_GAUSS_SIGMA", PS_META_REPLACE, "moments limit", Sigma);
+    psMetadataAddF32(analysis, PS_LIST_TAIL, "PSF_MOMENTS_RADIUS", PS_META_REPLACE, "moments limit", 4.0*Sigma);
+
+    psLogMsg ("psphot", 3, "using window function with sigma = %f\n", Sigma);
     return true;
 }
-# endif
+
+// if we use the footprints, the output peaks list contains both old and new peaks,
+// otherwise it only contains the new peaks.
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotStack.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStack.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStack.c	(revision 27840)
@@ -0,0 +1,35 @@
+# include "psphotStandAlone.h"
+
+int main (int argc, char **argv) {
+
+    psTimerStart ("complete");
+    pmErrorRegister();                  // register psModule's error codes/messages
+    psphotInit();
+
+    // load command-line arguments, options, and system config data
+    pmConfig *config = psphotStackArguments (argc, argv);
+    assert(config);
+
+    psphotVersionPrint();
+
+    // load input data (config and images (signal, noise, mask)
+    if (!psphotStackParseCamera (config)) {
+        psErrorStackPrint(stderr, "Error setting up the camera\n");
+        exit (psphotGetExitStatus());
+    }
+
+    // call psphot for each readout
+    if (!psphotStackImageLoop (config)) {
+        psErrorStackPrint(stderr, "Error in the psphot image loop\n");
+        exit (psphotGetExitStatus());
+    }
+
+    psLogMsg ("psphot", 3, "complete psphot run: %f sec\n", psTimerMark ("complete"));
+
+    psErrorCode exit_status = psphotGetExitStatus();
+    psphotCleanup (config);
+    exit (exit_status);
+}
+
+// all functions which return to this level must raise one of the top-level error codes if they
+// exit with an error.  these error codes are used to specify the program exit status
Index: branches/simtest_nebulous_branches/psphot/src/psphotStackArguments.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStackArguments.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStackArguments.c	(revision 27840)
@@ -0,0 +1,145 @@
+# include "psphotStandAlone.h"
+
+static void usage(pmConfig *config, int exitCode);
+static void writeHelpInfo(FILE* ofile);
+
+pmConfig *psphotStackArguments(int argc, char **argv) {
+
+    int N;
+
+    // print help info
+    if (psArgumentGet(argc, argv, "-help")) writeHelpInfo(stdout);
+    if (psArgumentGet(argc, argv, "-h")) writeHelpInfo(stdout);
+
+    // load config data from default locations
+    pmConfig *config = pmConfigRead(&argc, argv, PSPHOT_RECIPE);
+    if (config == NULL) {
+      psErrorStackPrint(stderr, "Can't read site configuration");
+	exit(PS_EXIT_CONFIG_ERROR);
+    }
+
+    // -version and -dumpconfig arguments
+    PSARGUMENTS_INSTANTIATE_GENERICS( psphot, config, argc, argv );
+
+    // save the following additional recipe values based on command-line options
+    // these options override the PSPHOT recipe values loaded from recipe files
+    psMetadata *options = pmConfigRecipeOptions (config, PSPHOT_RECIPE);
+
+    // Number of threads is handled
+    PSARGUMENTS_INSTANTIATE_THREADSARG( psphot, config, argc, argv )
+
+    // visual : interactive display mode
+    if ((N = psArgumentGet (argc, argv, "-visual"))) {
+        psArgumentRemove (N, &argc, argv);
+        pmVisualSetVisual(true);
+    }
+
+    // break : used from recipe throughout psphotReadout to stop processing early
+    if ((N = psArgumentGet (argc, argv, "-break"))) {
+	if (argc <= N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove (N, &argc, argv);
+        psMetadataAddStr (options, PS_LIST_TAIL, "BREAK_POINT", PS_META_REPLACE, "", argv[N]);
+        psArgumentRemove (N, &argc, argv);
+    }
+
+    // find the input data file (an mdc file)
+    if ((N = psArgumentGet(argc, argv, "-input"))) {
+	if (argc <= N+1) {
+	  psErrorStackPrint(stderr, "Expected to see 1 more argument; saw %d", argc - 1);
+	  exit(PS_EXIT_CONFIG_ERROR);
+	}
+        psArgumentRemove(N, &argc, argv);
+
+        unsigned int numBad = 0;                     // Number of bad lines
+        psMetadata *inputs = psMetadataConfigRead(NULL, &numBad, argv[N], false); // Input file info
+        if (!inputs || numBad > 0) {
+	    psErrorStackPrint(stderr, "Unable to cleanly read MDC file with inputs.");
+	    exit(PS_EXIT_CONFIG_ERROR);
+        }
+        psMetadataAddMetadata(config->arguments, PS_LIST_TAIL, "INPUTS", 0, "Metadata with input details", inputs);
+        psFree(inputs);
+
+        psArgumentRemove(N, &argc, argv);
+    }
+
+    if (argc != 2) {
+        psError(PSPHOT_ERR_ARGUMENTS, true, "Expected to see one more argument; saw %d", argc - 1);
+	usage(config, PS_EXIT_CONFIG_ERROR);
+    }
+
+    // output position is fixed
+    psMetadataAddStr (config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "", argv[1]);
+
+    psTrace("psphot", 1, "Done with psphotArguments...\n");
+    return (config);
+}
+
+static void writeHelpInfo(FILE* ofile)
+{
+  fprintf(ofile,
+	  "Usage: psphotStack -input (INPUTS.mdc) (OUTROOT)\n"
+	  "\n"
+	  "where INPUTS.mdc contains various METADATAs, each with:\n"
+	  "\tIMAGE(STR):     Image filename\n"
+	  "\tMASK(STR):      Mask filename\n"
+	  "\tVARIANCE(STR):  Variance map filename\n"
+	  "OUTROOT is the 'root name' for output files\n"
+	  "\n"
+	  "additional options:\n"
+	  "  -chip nn[,nn,...]\n"
+	  "     select detector chips to process; default is all.\n"
+	  "     Indices correspond to zero-based offset in the FPA metadata table.\n"
+	  "  -photcode PhotoCodeName\n"
+	  "     specify photocode\n"
+	  "  -region RegionString\n"
+	  "     specify analysis region.  String is of form '[x0:x1,y0:y1]'\n"
+	  "     To use this option you must define a default in psphot.config\n"
+	  "  -visual\n"
+	  "     turns on interactive display mode\n"
+	  "  -dumpconfig CfgFileName\n"
+          "     causes config info to be dumped to the named file.\n"
+	  "  -break NOTHING|BACKMDL|PEAKS|MOMENTS|PSFMODEL|ENSEMBLE|PASS1\n"
+	  "     choose a point at which to exit processing early\n"
+	  "  -nthreads n\n"
+	  "     set number of parallel threads of execution\n"
+	  "  -F OldFileRule ReplacementFileRule\n"
+	  "     change file naming rule; e.g. '-F PSPHOT.OUTPUT PSPHOT.OUT.CMF.MEF'\n"
+	  "  -D name stringval\n"
+	  "     set a string-valued config parameter\n"
+	  "  -Di name intval\n"
+	  "     set an integer-valued config parameter\n"
+	  "  -Df name fval\n"
+	  "     set a float-valued config parameter\n"
+	  "  -Db name boolval\n"
+	  "     set a boolean-valued config parameter\n"
+	  "  -v, -vv, -vvv\n"
+	  "     set increasing levels of verbosity\n"
+	  "  -logfmt FormatString\n"
+	  "     set format string used for log messages\n"
+	  "  -trace Fac Lvl\n"
+	  "     set tracing for facility Fac to integer Lvl, e.g. '-trace err 10'\n"
+	  "  -trace-levels\n"
+	  "     print current trace levels\n");
+    psLibFinalize();
+    exit(PS_EXIT_SUCCESS);
+}
+
+static void usage(pmConfig *config,      // Configuration
+		  int exitCode
+		  ) 
+{
+    fprintf(stderr, 
+	    "Usage: psphotStack -input input.mdc outroot\n"
+	    "Try 'psphotStack -help' for more options and explanation\n");
+
+    if (exitCode != PS_EXIT_SUCCESS) psErrorStackPrint(stderr, "Error reading arguments\n");
+
+    psFree(config);
+    pmConfigDone();
+    psLibFinalize();
+    exit(exitCode);
+}
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotStackChisqImage.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStackChisqImage.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStackChisqImage.c	(revision 27840)
@@ -0,0 +1,181 @@
+# include "psphotInternal.h"
+
+// Chi = sqrt(\sum_i im_i^2/var_i)
+
+static int npass = 0;
+
+// XXX supply filename or keep PSPHOT.INPUT fixed?
+bool psphotStackChisqImage (pmConfig *config, const pmFPAview *view)
+{
+    bool status = false;
+
+    psTimerStart ("psphot.chisq.image");
+
+    pmFPAfile *chisqFile = pmFPAfileSelectSingle(config->files, "PSPHOT.CHISQ.IMAGE", 0);
+    psAssert (chisqFile, "missing chisq image FPA?");
+
+    pmReadout *chiReadout = NULL;
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+        if (!psphotStackChisqImageAddReadout(config, view, &chiReadout, "PSPHOT.INPUT", i)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to model background for PSPHOT.INPUT entry %d", i);
+            return false;
+        }
+    }
+
+    num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "PSPHOT.CHISQ.NUM", PS_META_REPLACE, "", num);
+    num++;
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "PSPHOT.INPUT.NUM", PS_META_REPLACE, "", num);
+
+    // need to save the resulting image somewhere (PSPHOT.INPUT?)
+    if (!psMetadataAddPtr(config->files, PS_LIST_TAIL, "PSPHOT.INPUT", PS_DATA_UNKNOWN | PS_META_DUPLICATE_OK, "", chisqFile)) {
+        psError(PM_ERR_CONFIG, false, "could not add chisqFPA to config files");
+        return false;
+    }
+
+    psLogMsg ("psphot", PS_LOG_INFO, "built chisq image: %f sec\n", psTimerMark ("psphot.chisq.image"));
+
+    return true;
+}
+
+bool psphotStackChisqImageAddReadout(const pmConfig *config, // Configuration
+				     const pmFPAview *view,
+				     pmReadout **chiReadout,
+				     char *filename, 
+				     int index) 
+{
+    bool status = true;
+
+    // find the currently selected readout
+    pmFPAfile *input = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+    psAssert (input, "missing file?");
+
+    pmReadout *inReadout = pmFPAviewThisReadout(view, input->fpa);
+    psAssert (inReadout, "missing readout?");
+
+    psImage *inImage     = inReadout->image;
+    psAssert (inImage, "missing image?");
+
+    psImage *inVariance  = inReadout->variance;
+    psAssert (inVariance, "missing variance?");
+
+    psImage *inMask      = inReadout->mask;
+    psAssert (inMask, "missing mask?");
+
+    if (*chiReadout == NULL) {
+	*chiReadout = pmFPAGenerateReadout(config, view, "PSPHOT.CHISQ.IMAGE", input->fpa, NULL, 0);
+    }
+
+    psImage *chiImage = (*chiReadout)->image;
+    psAssert (chiImage, "missing chi image");
+
+    psImage *chiVariance = (*chiReadout)->variance;
+    psAssert (chiVariance, "missing chi variance");
+
+    psImage *chiMask = (*chiReadout)->mask;
+    psAssert (chiMask, "missing chi mask");
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    assert (recipe);
+
+    // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
+    psImageMaskType maskVal = psMetadataLookupImageMask(&status, recipe, "MASK.PSPHOT"); // Mask value for bad pixels
+    assert (maskVal);
+
+    // generate the chisq image:
+    for (int iy = 0; iy < inImage->numRows; iy++) {
+        for (int ix = 0; ix < inImage->numCols; ix++) {
+	    if (inMask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] & maskVal) continue;
+	    chiImage->data.F32[iy][ix] += PS_SQR(inImage->data.F32[iy][ix]) / inVariance->data.F32[iy][ix];
+	    chiVariance->data.F32[iy][ix] = 1.0; // ?? what is the right value?  just init to this?
+	    chiMask->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix] = 0x00; // we have valid data so unmask this pixel
+        }
+    }
+
+    if (psTraceGetLevel("psphot") > 5) {
+        char name[256];
+        sprintf (name, "chiImage.%02d.fits", npass);
+        psphotSaveImage (NULL, chiImage, name);
+    }
+    npass ++;
+
+    return true;
+}
+
+bool psphotStackRemoveChisqFromInputs (pmConfig *config) {
+
+    bool status = false;
+
+    int chisqNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.CHISQ.NUM");
+    psAssert (status, "programming error: must define PSPHOT.CHISQ.NUM");
+
+    int inputNum = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    pmFPAfileRemoveSingle (config->files, "PSPHOT.INPUT", chisqNum);
+
+    inputNum --;
+    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "PSPHOT.INPUT.NUM", PS_META_REPLACE, "", inputNum);
+
+    return true;
+}
+
+bool pmFPAfileRemoveSingle(psMetadata *files, const char *name, int num)
+{
+    PS_ASSERT_PTR_NON_NULL(files, NULL);
+    PS_ASSERT_INT_NONNEGATIVE(num, NULL);
+
+    psList* mdList = files->list;
+    psHash* mdHash = files->hash;
+
+    // Generate a REGEX to select only items that match 'name'
+    psString regex = NULL;              // Regular expression
+    if (name) {
+        if (!psMetadataLookup(files, name)) {
+            // No files match the requested name
+            return false;
+        }
+        psStringAppend(&regex, "^%s$", name);
+    }
+
+    psMetadataIterator *iter = psMetadataIteratorAlloc(files, PS_LIST_HEAD, regex); // Iterator
+    psFree(regex);
+    psMetadataItem *item;               // Item from iteration
+
+    bool found = false;
+    int i = 0;                          // Counter
+    for (i = 0; !found && (item = psMetadataGetAndIncrement(iter)); i++) {
+        if (i == num) found = true;
+    }
+    psFree(iter);
+    if (!found) {
+	return false;
+    }
+
+    char *key = item->name;
+
+    // look up the name via hash to see if we have a multi or not
+    psMetadataItem* hashItem = psHashLookup(mdHash, name);
+    if (hashItem == NULL) {
+        psError(PS_ERR_UNKNOWN, false, _("Failed to remove metadata item, %s, from metadata table."), key);
+        return false;
+    }
+
+    if (hashItem->type == PS_DATA_METADATA_MULTI) {
+        // multiple entries with same key, remove just the specified one
+        psListRemoveData(hashItem->data.list, item);
+    } else {
+        psHashRemove(mdHash, key);
+    }
+    psListRemoveData(mdList, item);
+
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotStackImageLoop.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStackImageLoop.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStackImageLoop.c	(revision 27840)
@@ -0,0 +1,79 @@
+# include "psphotStandAlone.h"
+
+# define ESCAPE(MESSAGE) { \
+  psError(PSPHOT_ERR_DATA, false, MESSAGE); \
+  psFree (view); \
+  return false; \
+}
+
+bool psphotStackImageLoop (pmConfig *config) {
+
+    bool status;
+    pmChip *chip;
+    pmCell *cell;
+    pmReadout *readout;
+
+    pmFPAfile *input = psMetadataLookupPtr (&status, config->files, "PSPHOT.INPUT");
+    if (!status) {
+        psError(PSPHOT_ERR_PROG, false, "Can't find input data!");
+        return false;
+    }
+
+    pmFPAview *view = pmFPAviewAlloc (0);
+
+    // XXX for now, just load the full set of images up front
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for fpa in psphot.");
+
+    // for psphot, we force data to be read at the chip level
+    while ((chip = pmFPAviewNextChip (view, input->fpa, 1)) != NULL) {
+        psLogMsg ("psphot", 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 ("failed input for Chip in psphotStack.");
+
+        // there is now only a single chip (multiple readouts?). loop over it and process
+        while ((cell = pmFPAviewNextCell (view, input->fpa, 1)) != NULL) {
+            psLogMsg ("psphot", 5, "Cell %d: %x %x\n", view->cell, cell->file_exists, cell->process);
+	    if (!pmFPAfileIOChecks (config, view, PM_FPA_BEFORE)) ESCAPE ("failed input for Cell in psphotStack.");
+
+            // process each of the readouts
+            while ((readout = pmFPAviewNextReadout (view, input->fpa, 1)) != NULL) {
+                psLogMsg ("psphot", 6, "Readout %d: %x %x\n", view->readout, cell->file_exists, cell->process);
+                if (! readout->data_exists) { continue; }
+
+		// XXX for now, we assume there is only a single chip in the PHU:
+		if (!psphotStackReadout (config, view)) {
+                    psError(psErrorCodeLast(), false, "failure in psphotReadout for chip %d, cell %d, readout %d\n", view->chip, view->cell, view->readout);
+                    psFree (view);
+                    return false;
+		}
+
+	    }
+	    // drop all versions of the internal files
+	    status = true;
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL");
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKMDL.STDEV");
+	    status &= pmFPAfileDropInternal (config->files, "PSPHOT.BACKGND");
+	    if (!status) {
+		psError(PSPHOT_ERR_PROG, false, "trouble dropping internal files");
+		psFree (view);
+		return false;
+	    }
+	}
+	// save output which is saved at the chip level
+	if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed output for Chip in psphot.");
+    }
+    // save output which is saved at the fpa level
+    if (!pmFPAfileIOChecks (config, view, PM_FPA_AFTER)) ESCAPE ("failed ouput for FPA in psphot.");
+
+    // fail if we failed to handle an error
+    if (psErrorCodeLast() != PS_ERR_NONE) psAbort ("failed to handle an error!");
+
+    psFree (view);
+    return true;
+}
+
+/* 
+   the easiest way to implement this is to assume we can pre-load the full set of images up front.
+   with 5 filters and 6000^2 (image, mask, var = 10 byte per pixel), we need 1.8GB, which is not too bad.
+*/
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotStackParseCamera.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStackParseCamera.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStackParseCamera.c	(revision 27840)
@@ -0,0 +1,146 @@
+# include "psphotStandAlone.h"
+
+static pmFPAfile *defineFile(pmConfig *config, pmFPAfile *bind, const char *name, const char *filename, pmFPAfileType type);
+
+// define the needed / desired I/O files
+bool psphotStackParseCamera (pmConfig *config) {
+
+    bool status = false;
+
+    // the input images are defined as a set of metadatas in the INPUTS metadata folder
+    psMetadata *inputs = psMetadataLookupMetadata(&status, config->arguments, "INPUTS"); // The inputs info
+    if (!inputs) {
+	psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find inputs.");
+	return false;
+    }
+
+    int nInputs = inputs->list->n;
+    for (int i = 0; i < nInputs; i++) {
+	psMetadataItem *item = psMetadataGet(inputs, i);
+	if (item->type != PS_DATA_METADATA) {
+	    psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Component %s of the input metadata is not of type METADATA", item->name);
+	    return false;
+	}
+
+	psMetadata *input = item->data.md; // The input metadata of interest
+
+	// look for 'IMAGE', 'MASK', 'VARIANCE' in folder (only 'IMAGE' is required)
+
+	psString image = psMetadataLookupStr(NULL, input, "IMAGE"); // Name of image
+	if (!image || strlen(image) == 0) {
+	    psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Component %s lacks IMAGE of type STR", item->name);
+	    return false;
+	}
+	pmFPAfile *imageFile = defineFile(config, NULL, "PSPHOT.INPUT", image, PM_FPA_FILE_IMAGE); // File for image
+	if (!imageFile) {
+	    psError(PS_ERR_UNKNOWN, false, "Unable to define file from image %d (%s)", i, image);
+	    return false;
+	}
+
+	psString mask = psMetadataLookupStr(&status, input, "MASK"); // Name of mask
+	if (mask && strlen(mask) > 0) {
+	    if (!defineFile(config, imageFile, "PSPHOT.MASK", mask, PM_FPA_FILE_MASK)) {
+		psError(PS_ERR_UNKNOWN, false, "Unable to define file from mask %d (%s)", i, mask);
+		return false;
+	    }
+	}
+
+	psString variance = psMetadataLookupStr(&status, input, "VARIANCE"); // Name of variance map
+	if (variance && strlen(variance) > 0) {
+	    if (!defineFile(config, imageFile, "PSPHOT.VARIANCE", variance, PM_FPA_FILE_VARIANCE)) {
+		psError(PS_ERR_UNKNOWN, false, "Unable to define file from variance %d (%s)", i, variance);
+		return false;
+	    }
+	}
+	// the output sources are carried on the input->fpa structures
+	pmFPAfile *outsources = pmFPAfileDefineOutputFromFile (config, imageFile, "PSPHOT.STACK.OUTPUT");
+	if (!outsources) {
+	    psError(PSPHOT_ERR_CONFIG, false, "Cannot find a rule for PSPHOT.STACK.OUTPUT");
+	    return false;
+	}
+	outsources->save = true;
+	outsources->fileID = i;		// this is used to generate output names
+    }
+    psMetadataRemoveKey(config->arguments, "FILENAMES");
+    psMetadataAddS32 (config->arguments, PS_LIST_TAIL, "PSPHOT.INPUT.NUM", PS_META_REPLACE, "number of inputs", nInputs);
+
+    if (!psphotSetMaskBits (config)) {
+        psError (PS_ERR_UNKNOWN, false, "failed to set mask bit values");
+        return NULL;
+    }
+
+    // generate an pmFPAimage for the chisqImage
+    pmFPAfile *chisqImage = pmFPAfileDefineOutput(config, NULL, "PSPHOT.CHISQ.IMAGE");
+    if (!chisqImage) {
+        psError(PSPHOT_ERR_CONFIG, false, "Trouble defining PSPHOT.CHISQ.IMAGE");
+        return false;
+    }
+    chisqImage->save = true;
+
+    pmFPAfile *chisqMask = pmFPAfileDefineOutput(config, chisqImage->fpa, "PSPHOT.CHISQ.MASK");
+    if (!chisqMask) {
+        psError(PS_ERR_IO, false, _("Unable to generate output file from PSPHOT.CHISQ.MASK"));
+        return NULL;
+    }
+    if (chisqMask->type != PM_FPA_FILE_MASK) {
+        psError(PS_ERR_IO, true, "PSPHOT.CHISQ.MASK is not of type MASK");
+        return NULL;
+    }
+    chisqMask->save = true;
+
+    pmFPAfile *chisqVariance = pmFPAfileDefineOutput(config, chisqImage->fpa, "PSPHOT.CHISQ.VARIANCE");
+    if (!chisqVariance) {
+        psError(PS_ERR_IO, false, _("Unable to generate output file from PSPHOT.CHISQ.VARIANCE"));
+        return NULL;
+    }
+    if (chisqVariance->type != PM_FPA_FILE_VARIANCE) {
+        psError(PS_ERR_IO, true, "PSPHOT.CHISQ.VARIANCE is not of type VARIANCE");
+        return NULL;
+    }
+    chisqVariance->save = true;
+
+# if (0)    
+    // define the additional input/output files associated with psphot
+    // XXX figure out which files are needed by psphotStack
+    if (false && !psphotDefineFiles (config, input)) {
+        psError(PSPHOT_ERR_CONFIG, false, "Trouble defining the additional input/output files");
+        return false;
+    }
+# endif
+
+    psTrace("psphot", 1, "Done with psphotStackParseCamera...\n");
+
+    psErrorClear();                     // some metadata lookup may have failed
+    return true;
+}
+
+// Define a file
+static pmFPAfile *defineFile(pmConfig *config, // Configuration
+                             pmFPAfile *bind, // File to which to bind
+                             const char *name, // Name of file rule
+                             const char *filename, // Name of file
+                             pmFPAfileType type // Type of file
+                             )
+{
+
+    psArray *dummy = psArrayAlloc(1);   // Dummy array of filenames for this FPA
+    dummy->data[0] = psStringCopy(filename);
+    psMetadataAddArray(config->arguments, PS_LIST_TAIL, "FILENAMES", PS_META_REPLACE,
+                       "Filenames for file rule definition", dummy);
+    psFree(dummy);
+
+    bool found = false;             // Found the file?
+    pmFPAfile *file = bind ? pmFPAfileBindFromArgs(&found, bind, config, name, "FILENAMES") :
+        pmFPAfileDefineFromArgs(&found, config, name, "FILENAMES");
+    if (!file || !found) {
+        psError(PS_ERR_UNKNOWN, false, "Unable to define file %s from %s", name, filename);
+        return NULL;
+    }
+    if (file->type != type) {
+        psError(PS_ERR_IO, true, "%s is not of type %s", name, pmFPAfileStringFromType(type));
+        return NULL;
+    }
+
+    return file;
+}
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotStackReadout.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotStackReadout.c	(revision 27840)
+++ branches/simtest_nebulous_branches/psphot/src/psphotStackReadout.c	(revision 27840)
@@ -0,0 +1,169 @@
+# include "psphotInternal.h"
+
+bool psphotStackReadout (pmConfig *config, const pmFPAview *view) {
+
+    psTimerStart ("psphotReadout");
+
+    pmModelClassSetLimits(PM_MODEL_LIMITS_LAX); // allow models to have ugly fits (eg, central cusp)
+
+    // select the current recipe
+    psMetadata *recipe = psMetadataLookupPtr (NULL, config->recipes, PSPHOT_RECIPE);
+    if (!recipe) {
+        psError(PSPHOT_ERR_CONFIG, false, "missing recipe %s", PSPHOT_RECIPE);
+        return false;
+    }
+    // optional break-point for processing
+    char *breakPt = psMetadataLookupStr (NULL, recipe, "BREAK_POINT");
+    PS_ASSERT_PTR_NON_NULL (breakPt, false);
+
+    // set the photcode for each image
+    if (!psphotAddPhotcode (config, view)) {
+        psError (PSPHOT_ERR_CONFIG, false, "trouble defining the photcode");
+        return false;
+    }
+
+    // Generate the mask and weight images
+    if (!psphotSetMaskAndVariance (config, view)) {
+	return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "NOTHING")) {
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // generate a background model (median, smoothed image)
+    // XXX I think this is not defined correctly for an array of images.
+    if (!psphotModelBackground (config, view)) {
+	return psphotReadoutCleanup (config, view);
+    }
+    if (!psphotSubtractBackground (config, view)) {
+	return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "BACKMDL")) {
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // load the psf model, if suppled.  FWHM_X,FWHM_Y,etc are determined and saved on
+    // readout->analysis XXX this function currently only works with a single PSPHOT.INPUT
+    if (!psphotLoadPSF (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "error loading psf model");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    if (!psphotStackChisqImage(config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failure to generate chisq image");
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "CHISQ")) {
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // find the detections (by peak and/or footprint) in the image.
+    // This finds the detections on Chisq image as well as the individuals
+    if (!psphotFindDetections (config, view, true)) { // pass 1
+        // this only happens if we had an error in psphotFindDetections
+        psError (PSPHOT_ERR_UNKNOWN, false, "failure in peak analysis");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // construct sources and measure basic stats (saved on detections->newSources)
+    // only run this on detections from the input images, not chisq image
+    if (!psphotSourceStats (config, view, true)) { // pass 1
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // *** generate the objects (which unify the sources from the different images)
+    psArray *objects = psphotMatchSources (config, view);
+
+    // construct sources for the newly-generated sources (from other images)
+    if (!psphotSourceStats (config, view, false)) { // pass 1
+        psError(PSPHOT_ERR_UNKNOWN, false, "failure to generate sources");
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // find blended neighbors of very saturated stars (detections->newSources)
+    // if (!psphotDeblendSatstars (config, view)) {
+    //     psError (PSPHOT_ERR_UNKNOWN, false, "failed on satstar deblend analysis");
+    //     return psphotReadoutCleanup (config, view);
+    // }
+
+    // mark blended peaks PS_SOURCE_BLEND (detections->newSources)
+    // if (!psphotBasicDeblend (config, view)) {
+    //     psError (PSPHOT_ERR_UNKNOWN, false, "failed on deblend analysis");
+    //     return psphotReadoutCleanup (config, view);
+    // }
+
+    // classify sources based on moments, brightness
+    // only run this on detections from the input images, not chisq image
+    if (!psphotRoughClass (config, view)) {
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to determine rough classifications");
+	return psphotReadoutCleanup (config, view);
+    }
+    // if we were not supplied a PSF model, determine the IQ stats here (detections->newSources)
+    // only run this on detections from the input images, not chisq image
+    if (!psphotImageQuality (config, view)) { // pass 1
+        psError (PSPHOT_ERR_UNKNOWN, false, "failed to measure image quality");
+        return psphotReadoutCleanup(config, view);
+    }
+    if (!strcasecmp (breakPt, "MOMENTS")) {
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // use bright stellar objects to measure PSF if we were supplied a PSF for any input file,
+    // this step is skipped
+    if (!psphotChoosePSF (config, view)) { // pass 1
+        psLogMsg ("psphot", 3, "failure to construct a psf model");
+        return psphotReadoutCleanup (config, view);
+    }
+    if (!strcasecmp (breakPt, "PSFMODEL")) {
+        return psphotReadoutCleanup (config, view);
+    }
+
+    // include externally-supplied sources
+    // XXX fix this in the new multi-input context
+    // psphotLoadExtSources (config, view); // pass 1
+
+    // construct an initial model for each object, set the radius to fitRadius, set circular fit mask
+    psphotGuessModels (config, view);
+
+    // merge the newly selected sources into the existing list
+    // NOTE: merge OLD and NEW
+    psphotMergeSources (config, view);
+
+    // linear PSF fit to source peaks, subtract the models from the image (in PSF mask)
+    psphotFitSourcesLinearStack (config, objects, FALSE);
+    psFree (objects);
+
+    // identify CRs and extended sources
+    psphotSourceSize (config, view, TRUE);
+
+    // measure aperture photometry corrections
+    if (!psphotApResid (config, view)) {
+        psLogMsg ("psphot", 3, "failed on psphotApResid");
+	return psphotReadoutCleanup (config, view);
+    }
+
+    // calculate source magnitudes
+    psphotMagnitudes(config, view);
+
+    if (!psphotEfficiency(config, view)) {
+        psErrorStackPrint(stderr, "Unable to determine detection efficiencies from fake sources");
+        psErrorClear();
+    }
+
+    // replace failed sources?
+    // psphotReplaceUnfitSources (sources);
+
+    // replace background in residual image
+    psphotSkyReplace (config, view);
+
+    // drop the references to the image pixels held by each source
+    psphotSourceFreePixels (config, view);
+
+    // remove chisq image from config->file:PSPHOT.INPUT (why?)
+    psphotStackRemoveChisqFromInputs(config);
+
+    // create the exported-metadata and free local data
+    return psphotReadoutCleanup (config, view);
+}
+
Index: branches/simtest_nebulous_branches/psphot/src/psphotSubtractBackground.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotSubtractBackground.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotSubtractBackground.c	(revision 27840)
@@ -4,5 +4,5 @@
 // generate the median in NxN boxes, clipping heavily
 // linear interpolation to generate full-scale model
-bool psphotSubtractBackground (pmConfig *config, const pmFPAview *view, const char *filename)
+bool psphotSubtractBackgroundReadout (pmConfig *config, const pmFPAview *view, const char *filename, int index, psMetadata *recipe)
 {
     bool status = true;
@@ -13,5 +13,6 @@
 
     // find the currently selected readout
-    pmFPAfile *file = psMetadataLookupPtr (&status, config->files, filename);
+    pmFPAfile *file = pmFPAfileSelectSingle(config->files, filename, index); // File of interest
+
     pmFPA *inFPA = file->fpa;
     pmReadout *readout = pmFPAviewThisReadout (view, inFPA);
@@ -19,11 +20,15 @@
     psImage *mask  = readout->mask;
 
-    // find the currently selected readout
-    pmReadout *model = pmFPAfileThisReadout (config->files, view, "PSPHOT.BACKMDL");
+    // find the currently selected readout (XXX note that the model is saved on PSPHOT.BACKMDL regardless of 'filename'
+    pmFPAfile *modelFile = pmFPAfileSelectSingle(config->files, "PSPHOT.BACKMDL", index); // File of interest
+    assert (modelFile);
+
+    pmReadout *model = NULL;
+    if (modelFile->mode == PM_FPA_MODE_INTERNAL) {
+	model = modelFile->readout;
+    } else {
+	model = pmFPAviewThisReadout (view, modelFile->fpa);
+    }
     assert (model);
-
-    // select the appropriate recipe information
-    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
-    assert (recipe);
 
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
@@ -35,4 +40,5 @@
 
     // select background pixels, from output background file, or create
+    // XXX for now, we will only allow a single background image to be generated
     file = psMetadataLookupPtr (&status, config->files, "PSPHOT.BACKGND");
     if (file) {
@@ -66,4 +72,5 @@
 
     // back-sub image pixels, from output background file (don't create if not requested)
+    // XXX for now, we will only allow a single background-subtracted image to be generated
     file = psMetadataLookupPtr (&status, config->files, "PSPHOT.BACKSUB");
     if (file) {
@@ -109,5 +116,30 @@
     // the pmReadout selected in this function are all view on entries in config->files
 
+    // display the backsub and backgnd images
+    // move this inthe the subtract background loop
+    psphotVisualShowBackground (config, view, readout);
+
     npass ++;
     return true;
 }
+
+bool psphotSubtractBackground (pmConfig *config, const pmFPAview *view)
+{
+    bool status = false;
+
+    // select the appropriate recipe information
+    psMetadata *recipe  = psMetadataLookupPtr (&status, config->recipes, PSPHOT_RECIPE);
+    psAssert (recipe, "missing recipe?");
+
+    int num = psMetadataLookupS32 (&status, config->arguments, "PSPHOT.INPUT.NUM");
+    psAssert (status, "programming error: must define PSPHOT.INPUT.NUM");
+
+    // loop over the available readouts
+    for (int i = 0; i < num; i++) {
+	if (!psphotSubtractBackgroundReadout (config, view, "PSPHOT.INPUT", i, recipe)) {
+            psError (PSPHOT_ERR_CONFIG, false, "failed to subtract background for PSPHOT.INPUT entry %d", i);
+	    return false;
+	}
+    }
+    return true;
+}
Index: branches/simtest_nebulous_branches/psphot/src/psphotVersion.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotVersion.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotVersion.c	(revision 27840)
@@ -30,6 +30,6 @@
 psString psphotVersionLong(void)
 {
-    psString version = psLibVersion();  // Version, to return
-    psString source = psLibSource();    // Source
+    psString version = psphotVersion();  // Version, to return
+    psString source = psphotSource();    // Source
 
     psStringPrepend(&version, "psphot ");
Index: branches/simtest_nebulous_branches/psphot/src/psphotVisual.c
===================================================================
--- branches/simtest_nebulous_branches/psphot/src/psphotVisual.c	(revision 25061)
+++ branches/simtest_nebulous_branches/psphot/src/psphotVisual.c	(revision 27840)
@@ -15,11 +15,62 @@
 # include <kapa.h>
 
+bool pmVisualLimitsFromVectors (Graphdata *graphdata, psVector *xVec, psVector *yVec);
+
 // functions used to visualize the analysis as it goes
 // these are invoked by the -visual options
 
-static int kapa = -1;
+static int kapa1 = -1;
 static int kapa2 = -1;
 static int kapa3 = -1;
 
+int psphotKapaChannel (int channel) {
+
+    switch (channel) {
+      case 1:
+        if (kapa1 == -1) {
+            kapa1 = KapaOpenNamedSocket ("kapa", "psphot:images");
+            if (kapa1 == -1) {
+                fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+                pmVisualSetVisual(false);
+            }
+        }
+        return kapa1;
+      case 2:
+        if (kapa2 == -1) {
+            kapa2 = KapaOpenNamedSocket ("kapa", "psphot:plots");
+            if (kapa2 == -1) {
+                fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+                pmVisualSetVisual(false);
+            }
+        }
+        return kapa2;
+      case 3:
+        if (kapa3 == -1) {
+            kapa3 = KapaOpenNamedSocket ("kapa", "psphot:stamps");
+            if (kapa3 == -1) {
+                fprintf (stderr, "failure to open kapa; visual mode disabled\n");
+                pmVisualSetVisual(false);
+            }
+        }
+        return kapa3;
+      default:
+        psAbort ("unknown kapa channel");
+    }
+    psAbort ("unknown kapa channel");
+}
+
+bool psphotVisualEraseOverlays (int channel, char *overlay) {
+
+    int myKapa = psphotKapaChannel (channel);
+    if (!(strcasecmp (overlay, "all"))) {
+      KiiEraseOverlay (myKapa, "red");
+      KiiEraseOverlay (myKapa, "green");
+      KiiEraseOverlay (myKapa, "blue");
+      KiiEraseOverlay (myKapa, "yellow");
+      return true;
+    }
+    KiiEraseOverlay (myKapa, overlay);
+    return true;
+}
 
 bool psphotVisualShowMask (int kapaFD, psImage *inImage, const char *name, int channel) {
@@ -69,5 +120,5 @@
 }
 
-bool psphotVisualScaleImage (int kapaFD, psImage *inImage, const char *name, int channel) {
+bool psphotVisualScaleImage (int kapaFD, psImage *inImage, psImage *inMask, const char *name, int channel) {
 
     KiiImage image;
@@ -79,5 +130,5 @@
     psStats *stats = psStatsAlloc (PS_STAT_ROBUST_MEDIAN | PS_STAT_ROBUST_STDEV);
     psRandom *rng = psRandomAlloc(PS_RANDOM_TAUS);
-    if (!psImageBackground(stats, NULL, inImage, NULL, 0, rng)) {
+    if (!psImageBackground(stats, NULL, inImage, inMask, 0xffff, rng)) {
         fprintf (stderr, "failed to get background values\n");
         return false;
@@ -131,24 +182,12 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        kapa = KapaOpenNamedSocket ("kapa", "psphot:images");
-        if (kapa == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    // psphotVisualShowMask (kapa, readout->mask, "mask", 2);
-    psphotVisualScaleImage (kapa, readout->variance, "variance", 1);
-    psphotVisualScaleImage (kapa, readout->image, "image", 0);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
+
+    psphotVisualShowMask (kapa, readout->mask, "mask", 2);
+    psphotVisualScaleImage (kapa, readout->variance, readout->mask, "variance", 1);
+    psphotVisualScaleImage (kapa, readout->image, readout->mask, "image", 0);
+
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -160,12 +199,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        kapa = KapaOpenNamedSocket ("kapa", "psphot:images");
-        if (kapa == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
 
     bool status = false;
@@ -178,40 +211,21 @@
     }
 
-    psphotVisualScaleImage (kapa, backgnd->image, "backgnd", 2);
-    psphotVisualScaleImage (kapa, readout->image, "backsub", 0);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-    return true;
-}
-
-bool psphotVisualShowSignificance (psImage *image) {
+    psphotVisualScaleImage (kapa, backgnd->image, readout->mask, "backgnd", 2);
+    psphotVisualScaleImage (kapa, readout->image, readout->mask, "backsub", 0);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualShowSignificance (psImage *image, float min, float max) {
 
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        kapa = KapaOpenNamedSocket ("kapa", "psphot:images");
-        if (kapa == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    // XXX test: image->data.F32[10][10] = 10000;
-    psphotVisualRangeImage (kapa, image, "signif", 2, -1.0, 25.0*25.0);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
+
+    psphotVisualRangeImage (kapa, image, "signif", 2, min, max);
+
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -224,8 +238,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
 
     psArray *peaks = detections->peaks;
@@ -233,5 +245,5 @@
     // note: this uses the Ohana allocation tools:
     // ALLOCATE (overlay, KiiOverlay, 3*peaks->n + 1);
-    ALLOCATE (overlay, KiiOverlay, peaks->n);
+    ALLOCATE (overlay, KiiOverlay, peaks->n + 2);
 
     Noverlay = 0;
@@ -249,47 +261,10 @@
         overlay[Noverlay].text = NULL;
         Noverlay ++;
-
-# if (0)
-        overlay[Noverlay].type = KII_OVERLAY_BOX;
-        overlay[Noverlay].x = peak->x;
-        overlay[Noverlay].y = peak->y;
-        overlay[Noverlay].dx = 1.0;
-        overlay[Noverlay].dy = 1.0;
-        overlay[Noverlay].angle = 0.0;
-        overlay[Noverlay].text = NULL;
-        Noverlay ++;
-
-        overlay[Noverlay].type = KII_OVERLAY_CIRCLE;
-        overlay[Noverlay].x = peak->xf;
-        overlay[Noverlay].y = peak->yf;
-        overlay[Noverlay].dx = 2.0;
-        overlay[Noverlay].dy = 2.0;
-        overlay[Noverlay].angle = 0.0;
-        overlay[Noverlay].text = NULL;
-        Noverlay ++;
-# endif
-    }
-
-# if (0)
-    overlay[Noverlay].type = KII_OVERLAY_BOX;
-    overlay[Noverlay].x = 10.0;
-    overlay[Noverlay].y = 10.0;
-    overlay[Noverlay].dx = 0.5;
-    overlay[Noverlay].dy = 0.5;
-    overlay[Noverlay].angle = 0.0;
-    overlay[Noverlay].text = NULL;
-    Noverlay ++;
-# endif
+    }
 
     KiiLoadOverlay (kapa, overlay, Noverlay, "red");
     FREE (overlay);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -302,8 +277,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
 
     psArray *footprints = detections->footprints;
@@ -325,4 +298,5 @@
 
         // draw the top
+        // XXX need to allow top (and bottom) to have more than one span
         span = footprint->spans->data[0];
         overlay[Noverlay].type = KII_OVERLAY_LINE;
@@ -399,11 +373,5 @@
     FREE (overlay);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -419,8 +387,9 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
+
+    // XXX mark the different source classes with different color/shape dots
+    // XXX are moments S/N and peak S/N consistent?
 
     // note: this uses the Ohana allocation tools:
@@ -448,5 +417,7 @@
         overlay[Noverlay].dx = 2.0*axes.major;
         overlay[Noverlay].dy = 2.0*axes.minor;
-        overlay[Noverlay].angle = -axes.theta * PS_DEG_RAD;  // XXXXXXXX the axes angle is negative to display of object on kapa
+
+        overlay[Noverlay].angle = axes.theta * PS_DEG_RAD;
+
         overlay[Noverlay].text = NULL;
         Noverlay ++;
@@ -456,16 +427,9 @@
     FREE (overlay);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-
-    return true;
-}
-
-bool psphotVisualPlotMoments (psMetadata *recipe, psArray *sources) {
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualPlotMoments (psMetadata *recipe, psMetadata *analysis, psArray *sources) {
 
     bool status;
@@ -475,46 +439,48 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa3 == -1) {
-        kapa3 = KapaOpenNamedSocket ("kapa", "psphot:plots");
-        if (kapa3 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    KapaClearPlots (kapa3);
+    int myKapa = psphotKapaChannel (2);
+    if (myKapa == -1) return false;
+
+    KapaClearPlots (myKapa);
     KapaInitGraph (&graphdata);
-    KapaSetFont (kapa3, "courier", 14);
+    KapaSetFont (myKapa, "courier", 14);
+
+    section.bg = KapaColorByName ("none"); // XXX probably should be 'none'
 
     float SN_LIM = psMetadataLookupF32(&status, recipe, "PSF_SN_LIM");
 
     // select the max psfX,Y values for the plot limits
-    float Xmin = 0.0, Xmax = 0.0;
-    float Ymin = 0.0, Ymax = 0.0;
+    float Xmin = 1000.0, Xmax = 0.0;
+    float Ymin = 1000.0, Ymax = 0.0;
     {
-        int nRegions = psMetadataLookupS32 (&status, recipe, "PSF.CLUMP.NREGIONS");
+        int nRegions = psMetadataLookupS32 (&status, analysis, "PSF.CLUMP.NREGIONS");
         for (int n = 0; n < nRegions; n++) {
 
             char regionName[64];
             snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", n);
-            psMetadata *regionMD = psMetadataLookupPtr (&status, recipe, regionName);
-
-	    float psfX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
+            psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
+
+            float psfX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
             float psfY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");
             float psfdX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");
             float psfdY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");
 
-	    float X0 = psfX - 4.0*psfdX;
-	    float X1 = psfX + 4.0*psfdX;
-	    float Y0 = psfY - 4.0*psfdY;
-	    float Y1 = psfY + 4.0*psfdY;
-
-	    if (isfinite(X0)) { Xmin = PS_MAX(Xmin, X0); }
-	    if (isfinite(X1)) { Xmax = PS_MAX(Xmax, X1); }
-	    if (isfinite(Y0)) { Ymin = PS_MAX(Ymin, Y0); }
-	    if (isfinite(Y1)) { Ymax = PS_MAX(Ymax, Y1); }
-        }
-    }
+            float X0 = psfX - 4.0*psfdX;
+            float X1 = psfX + 4.0*psfdX;
+            float Y0 = psfY - 4.0*psfdY;
+            float Y1 = psfY + 4.0*psfdY;
+
+            if (isfinite(X0)) { Xmin = PS_MIN(Xmin, X0); }
+            if (isfinite(X1)) { Xmax = PS_MAX(Xmax, X1); }
+            if (isfinite(Y0)) { Ymin = PS_MIN(Ymin, Y0); }
+            if (isfinite(Y1)) { Ymax = PS_MAX(Ymax, Y1); }
+        }
+    }
+    Xmin = PS_MAX(Xmin, -0.1);
+    Ymin = PS_MAX(Ymin, -0.1);
+
+    // XXX test: hardwire plot limits
+    // Xmin = -0.1; Ymin = -0.1;
+    // Xmax = 20.1; Ymax = 20.1;
 
     // storage vectors for data to be plotted
@@ -564,5 +530,5 @@
     section.y  = 0.00;
     section.name = psStringCopy ("MxxMyy");
-    KapaSetSection (kapa3, &section);
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
 
@@ -572,9 +538,14 @@
     graphdata.xmax = Xmax;
     graphdata.ymax = Ymax;
-    KapaSetLimits (kapa3, &graphdata);
-
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "M_xx| (pixels)", KAPA_LABEL_XM);
-    KapaSendLabel (kapa3, "M_yy| (pixels)", KAPA_LABEL_YM);
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = NAN;
+    graphdata.padYm = NAN;
+    graphdata.padXp = 0.5;
+    graphdata.padYp = 0.5;
+    KapaBox (myKapa, &graphdata);
+
+    KapaSendLabel (myKapa, "M_xx| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "M_yy| (pixels)", KAPA_LABEL_YM);
 
     graphdata.color = KapaColorByName ("black");
@@ -582,7 +553,8 @@
     graphdata.size = 0.3;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nF, &graphdata);
-    KapaPlotVector (kapa3, nF, xFaint->data.F32, "x");
-    KapaPlotVector (kapa3, nF, yFaint->data.F32, "y");
+    KapaPrepPlot (myKapa, nF, &graphdata);
+
+    KapaPlotVector (myKapa, nF, xFaint->data.F32, "x");
+    KapaPlotVector (myKapa, nF, yFaint->data.F32, "y");
 
     graphdata.color = KapaColorByName ("red");
@@ -590,7 +562,7 @@
     graphdata.size = 0.5;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nB, &graphdata);
-    KapaPlotVector (kapa3, nB, xBright->data.F32, "x");
-    KapaPlotVector (kapa3, nB, yBright->data.F32, "y");
+    KapaPrepPlot (myKapa, nB, &graphdata);
+    KapaPlotVector (myKapa, nB, xBright->data.F32, "x");
+    KapaPlotVector (myKapa, nB, yBright->data.F32, "y");
 
     // second section: MagMyy
@@ -598,7 +570,7 @@
     section.dy = 0.25;
     section.x  = 0.00;
-    section.y  = 0.80;
+    section.y  = 0.75;
     section.name = psStringCopy ("MagMyy");
-    KapaSetSection (kapa3, &section);
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
 
@@ -608,10 +580,14 @@
     graphdata.ymin = Ymin;
     graphdata.ymax = Ymax;
-    KapaSetLimits (kapa3, &graphdata);
-
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = 0.5;
+    graphdata.padYm = NAN;
+    graphdata.padXp = NAN;
+    graphdata.padYp = 0.5;
     strcpy (graphdata.labels, "0210");
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "inst mag", KAPA_LABEL_XP);
-    KapaSendLabel (kapa3, "M_yy| (pixels)", KAPA_LABEL_YM);
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "inst mag", KAPA_LABEL_XP);
+    KapaSendLabel (myKapa, "M_yy| (pixels)", KAPA_LABEL_YM);
 
     graphdata.color = KapaColorByName ("black");
@@ -619,7 +595,7 @@
     graphdata.size = 0.3;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nF, &graphdata);
-    KapaPlotVector (kapa3, nF, mFaint->data.F32, "x");
-    KapaPlotVector (kapa3, nF, yFaint->data.F32, "y");
+    KapaPrepPlot (myKapa, nF, &graphdata);
+    KapaPlotVector (myKapa, nF, mFaint->data.F32, "x");
+    KapaPlotVector (myKapa, nF, yFaint->data.F32, "y");
 
     graphdata.color = KapaColorByName ("red");
@@ -627,15 +603,15 @@
     graphdata.size = 0.5;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nB, &graphdata);
-    KapaPlotVector (kapa3, nB, mBright->data.F32, "x");
-    KapaPlotVector (kapa3, nB, yBright->data.F32, "y");
+    KapaPrepPlot (myKapa, nB, &graphdata);
+    KapaPlotVector (myKapa, nB, mBright->data.F32, "x");
+    KapaPlotVector (myKapa, nB, yBright->data.F32, "y");
 
     // third section: MagMxx
     section.dx = 0.25;
     section.dy = 0.75;
-    section.x  = 0.80;
+    section.x  = 0.75;
     section.y  = 0.00;
     section.name = psStringCopy ("MagMxx");
-    KapaSetSection (kapa3, &section);
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
 
@@ -645,10 +621,14 @@
     graphdata.ymin =  -7.9;
     graphdata.ymax = -17.1;
-    KapaSetLimits (kapa3, &graphdata);
-
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = NAN;
+    graphdata.padYm = 0.5;
+    graphdata.padXp = 0.5;
+    graphdata.padYp = NAN;
     strcpy (graphdata.labels, "2001");
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "M_xx| (pixels)", KAPA_LABEL_XM);
-    KapaSendLabel (kapa3, "inst mag", KAPA_LABEL_YP);
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "M_xx| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "inst mag", KAPA_LABEL_YP);
 
     graphdata.color = KapaColorByName ("black");
@@ -656,7 +636,7 @@
     graphdata.size = 0.3;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nF, &graphdata);
-    KapaPlotVector (kapa3, nF, xFaint->data.F32, "x");
-    KapaPlotVector (kapa3, nF, mFaint->data.F32, "y");
+    KapaPrepPlot (myKapa, nF, &graphdata);
+    KapaPlotVector (myKapa, nF, xFaint->data.F32, "x");
+    KapaPlotVector (myKapa, nF, mFaint->data.F32, "y");
 
     graphdata.color = KapaColorByName ("red");
@@ -664,11 +644,11 @@
     graphdata.size = 0.5;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, nB, &graphdata);
-    KapaPlotVector (kapa3, nB, xBright->data.F32, "x");
-    KapaPlotVector (kapa3, nB, mBright->data.F32, "y");
+    KapaPrepPlot (myKapa, nB, &graphdata);
+    KapaPlotVector (myKapa, nB, xBright->data.F32, "x");
+    KapaPlotVector (myKapa, nB, mBright->data.F32, "y");
 
     // draw N circles to outline the clumps
     {
-        KapaSelectSection (kapa3, "MxxMyy");
+        KapaSelectSection (myKapa, "MxxMyy");
 
         // draw a circle centered on psfX,Y with size of the psf limit
@@ -676,5 +656,5 @@
         psVector *yLimit  = psVectorAlloc (120, PS_TYPE_F32);
 
-        int nRegions = psMetadataLookupS32 (&status, recipe, "PSF.CLUMP.NREGIONS");
+        int nRegions = psMetadataLookupS32 (&status, analysis, "PSF.CLUMP.NREGIONS");
         float PSF_CLUMP_NSIGMA = psMetadataLookupF32 (&status, recipe, "PSF_CLUMP_NSIGMA");
 
@@ -682,9 +662,9 @@
         graphdata.style = 0;
 
-	graphdata.xmin = Xmin;
-	graphdata.ymin = Ymin;
-	graphdata.xmax = Xmax;
-	graphdata.ymax = Ymax;
-	KapaSetLimits (kapa3, &graphdata);
+        graphdata.xmin = Xmin;
+        graphdata.ymin = Ymin;
+        graphdata.xmax = Xmax;
+        graphdata.ymax = Ymax;
+        KapaSetLimits (myKapa, &graphdata);
 
         for (int n = 0; n < nRegions; n++) {
@@ -692,5 +672,5 @@
             char regionName[64];
             snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", n);
-            psMetadata *regionMD = psMetadataLookupPtr (&status, recipe, regionName);
+            psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
 
             float psfX  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
@@ -705,47 +685,11 @@
                 yLimit->data.F32[i] = Ry*sin(i*2.0*M_PI/120.0) + psfY;
             }
-            KapaPrepPlot (kapa3, xLimit->n, &graphdata);
-            KapaPlotVector (kapa3, xLimit->n, xLimit->data.F32, "x");
-            KapaPlotVector (kapa3, yLimit->n, yLimit->data.F32, "y");
+            KapaPrepPlot (myKapa, xLimit->n, &graphdata);
+            KapaPlotVector (myKapa, xLimit->n, xLimit->data.F32, "x");
+            KapaPlotVector (myKapa, yLimit->n, yLimit->data.F32, "y");
         }
         psFree (xLimit);
         psFree (yLimit);
     }
-
-# if (0)
-    // *** make a histogram of the source counts in the x and y directions
-    psHistogram *nX = psHistogramAlloc (graphdata.xmin, graphdata.xmax, 50.0);
-    psHistogram *nY = psHistogramAlloc (graphdata.ymin, graphdata.ymax, 50.0);
-    psVectorHistogram (nX, xFaint, NULL, NULL, 0);
-    psVectorHistogram (nY, yFaint, NULL, NULL, 0);
-    psVector *dX = psVectorAlloc (nX->nums->n, PS_TYPE_F32);
-    psVector *vX = psVectorAlloc (nX->nums->n, PS_TYPE_F32);
-    psVector *dY = psVectorAlloc (nY->nums->n, PS_TYPE_F32);
-    psVector *vY = psVectorAlloc (nY->nums->n, PS_TYPE_F32);
-    for (int i = 0; i < nX->nums->n; i++) {
-        dX->data.F32[i] = nX->nums->data.S32[i];
-        vX->data.F32[i] = 0.5*(nX->bounds->data.F32[i] + nX->bounds->data.F32[i+1]);
-    }
-    for (int i = 0; i < nY->nums->n; i++) {
-        dY->data.F32[i] = nY->nums->data.S32[i];
-        vY->data.F32[i] = 0.5*(nY->bounds->data.F32[i] + nY->bounds->data.F32[i+1]);
-    }
-
-    graphdata.color = KapaColorByName ("black");
-    graphdata.ptype = 0;
-    graphdata.size = 0.0;
-    graphdata.style = 0;
-    KapaPrepPlot (kapa3, dX->n, &graphdata);
-    KapaPlotVector (kapa3, dX->n, dX->data.F32, "x");
-    KapaPlotVector (kapa3, vX->n, vX->data.F32, "y");
-
-    psFree (nX);
-    psFree (dX);
-    psFree (vX);
-
-    psFree (nY);
-    psFree (dY);
-    psFree (vY);
-# endif
 
     psFree (xBright);
@@ -756,16 +700,10 @@
     psFree (mFaint);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    pmVisualAskUser(NULL);
     return true;
 }
 
 // assumes 'kapa' value is checked and set
-bool psphotVisualShowRoughClass_Single (psArray *sources, pmSourceType type, pmSourceMode mode, char *color) {
+bool psphotVisualShowRoughClass_Single (int myKapa, psArray *sources, pmSourceType type, pmSourceMode mode, char *color) {
 
     int Noverlay;
@@ -802,10 +740,10 @@
         overlay[Noverlay].dx = 2.0*axes.major;
         overlay[Noverlay].dy = 2.0*axes.minor;
-        overlay[Noverlay].angle = -axes.theta * PS_DEG_RAD;
+        overlay[Noverlay].angle = axes.theta * PS_DEG_RAD;
         overlay[Noverlay].text = NULL;
         Noverlay ++;
     }
 
-    KiiLoadOverlay (kapa, overlay, Noverlay, color);
+    KiiLoadOverlay (myKapa, overlay, Noverlay, color);
     FREE (overlay);
 
@@ -817,26 +755,18 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
-
-    KiiEraseOverlay (kapa, "yellow"); // moments
-
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_STAR, 0, "red");
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_EXTENDED, 0, "blue");
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_DEFECT, 0, "blue");
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_SATURATED, 0, "red");
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_STAR, PM_SOURCE_MODE_PSFSTAR, "yellow");
-    psphotVisualShowRoughClass_Single (sources, PM_SOURCE_TYPE_STAR, PM_SOURCE_MODE_SATSTAR, "green");
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
+    int myKapa = psphotKapaChannel (1);
+    if (myKapa == -1) return false;
+
+    KiiEraseOverlay (myKapa, "yellow"); // moments
+
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_STAR, 0, "red");
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_EXTENDED, 0, "blue");
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_DEFECT, 0, "blue");
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_SATURATED, 0, "red");
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_STAR, PM_SOURCE_MODE_PSFSTAR, "yellow");
+    psphotVisualShowRoughClass_Single (myKapa, sources, PM_SOURCE_TYPE_STAR, PM_SOURCE_MODE_SATSTAR, "green");
+
     fprintf (stdout, "red: STAR or SAT AREA; blue: EXTENDED or DEFECT; green: SATSTAR; yellow: PSFSTAR\n");
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -846,12 +776,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa2 == -1) {
-        kapa2 = KapaOpenNamedSocket ("kapa", "psphot:psfstars");
-        if (kapa2 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
+    int myKapa = psphotKapaChannel (3);
+    if (myKapa == -1) return false;
 
     int DX = 64;
@@ -898,7 +822,7 @@
 
     psImage *psfLogFlux = (psImage *) psUnaryOp (NULL, psfMosaic, "log");
-    psphotVisualRangeImage (kapa2, psfLogFlux, "psf_mosaic",    0, -2.0, 3.0);
-    psphotVisualRangeImage (kapa2, funMosaic, "psf_analytical", 1, -10.0, 100.0);
-    psphotVisualRangeImage (kapa2, resMosaic, "psf_residual",   2, -10.0, 100.0);
+    psphotVisualRangeImage (myKapa, psfLogFlux, "psf_mosaic",    0, -2.0, 3.0);
+    psphotVisualRangeImage (myKapa, funMosaic, "psf_analytical", 1, -10.0, 100.0);
+    psphotVisualRangeImage (myKapa, resMosaic, "psf_residual",   2, -10.0, 100.0);
 
     psFree (psfMosaic);
@@ -908,11 +832,5 @@
     psFree (modelRef);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -924,12 +842,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa2 == -1) {
-        kapa2 = KapaOpenNamedSocket ("kapa", "psphot:psfstars");
-        if (kapa2 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
+    int myKapa = psphotKapaChannel (3);
+    if (myKapa == -1) return false;
 
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
@@ -1017,10 +929,12 @@
             if (Xo == 0) {
                 // place source alone on this row
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+                bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+                if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-                psphotSubWithTest (source, false, maskVal); // remove source (force)
+                pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
-                psphotSetState (source, false, maskVal); // reset source Add/Sub state to recorded
+
+                if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
                 Yo += DY;
@@ -1032,10 +946,12 @@
                 Xo = 0;
 
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+                bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+                if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-                psphotSubWithTest (source, false, maskVal); // remove source (force)
+                pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
-                psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+
+                if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
                 Xo = DX;
@@ -1044,10 +960,11 @@
         } else {
             // extend this row
-            psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+            bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+            if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
             psphotMosaicSubimage (outpos, source, Xo, Yo, DX, DY, true);
 
-            psphotSubWithTest (source, false, maskVal); // remove source (force)
+            pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
             psphotMosaicSubimage (outsub, source, Xo, Yo, DX, DY, true);
-            psphotSetState (source, false, maskVal); // replace source (has been subtracted)
+            if (!subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
             Xo += DX;
@@ -1056,15 +973,8 @@
     }
 
-    psphotVisualRangeImage (kapa2, outpos, "psfpos", 0, -0.05, 0.95);
-    psphotVisualRangeImage (kapa2, outsub, "psfsub", 1, -0.05, 0.95);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-
+    psphotVisualRangeImage (myKapa, outpos, "psfpos", 0, -0.05, 0.95);
+    psphotVisualRangeImage (myKapa, outsub, "psfsub", 1, -0.05, 0.95);
+
+    pmVisualAskUser(NULL);
     psFree (outpos);
     psFree (outsub);
@@ -1084,12 +994,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa2 == -1) {
-        kapa2 = KapaOpenNamedSocket ("kapa", "psphot:images");
-        if (kapa2 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
+    int myKapa = psphotKapaChannel (3);
+    if (myKapa == -1) return false;
 
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
@@ -1119,7 +1023,7 @@
         pmSource *source = sources->data[i];
 
-        bool keep = false;
-        keep |= (source->mode & PM_SOURCE_MODE_SATSTAR);
-        if (!keep) continue;
+        // only show "real" saturated stars (not defects)
+        if (!(source->mode & PM_SOURCE_MODE_SATSTAR)) continue;;
+        if (source->mode & PM_SOURCE_MODE_DEFECT) continue;;
 
         // how does this subimage get placed into the output image?
@@ -1164,10 +1068,8 @@
         pmSource *source = sources->data[i];
 
-        bool keep = false;
-        if (source->mode & PM_SOURCE_MODE_SATSTAR) {
-            nSAT ++;
-            keep = true;
-        }
-        if (!keep) continue;
+        // only show "real" saturated stars (not defects)
+        if (!(source->mode & PM_SOURCE_MODE_SATSTAR)) continue;;
+        if (source->mode & PM_SOURCE_MODE_DEFECT) continue;;
+        nSAT ++;
 
         if (Xo + DX > NX) {
@@ -1175,7 +1077,8 @@
             if (Xo == 0) {
                 // place source alone on this row
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+                bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+                if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsat, source, Xo, Yo, DX, DY, false);
-                psphotSetState (source, true, maskVal); // reset source Add/Sub state to recorded
+                if (subtracted) pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
 
                 Yo += DY;
@@ -1186,7 +1089,9 @@
                 Yo += dY;
                 Xo = 0;
-                psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+
+                bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+                if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
                 psphotMosaicSubimage (outsat, source, Xo, Yo, DX, DY, false);
-                psphotSetState (source, true, maskVal); // replace source (has been subtracted)
+                if (subtracted) pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
 
                 Xo = DX;
@@ -1195,7 +1100,8 @@
         } else {
             // extend this row
-            psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+            bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+            if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
             psphotMosaicSubimage (outsat, source, Xo, Yo, DX, DY, false);
-            psphotSetState (source, true, maskVal); // replace source (has been subtracted)
+            if (subtracted) pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
 
             Xo += DX;
@@ -1204,24 +1110,29 @@
     }
 
-    psphotVisualScaleImage (kapa2, outsat, "satstar", 2);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-
+    psphotVisualScaleImage (myKapa, outsat, NULL, "satstar", 2);
+
+    pmVisualAskUser(NULL);
     psFree (outsat);
     return true;
 }
 
+static void plotline (int myKapa, Graphdata *graphdata, float x0, float y0, float x1, float y1) 
+{
+    float x[2], y[2];
+    x[0] = x0;
+    x[1] = x1;
+    y[0] = y0;
+    y[1] = y1;
+    KapaPrepPlot   (myKapa, 2, graphdata);
+    KapaPlotVector (myKapa, 2, x, "x");
+    KapaPlotVector (myKapa, 2, y, "y");
+}
+
 bool psphotVisualPlotRadialProfile (int myKapa, pmSource *source, psImageMaskType maskVal) {
 
     Graphdata graphdata;
 
-    bool state = !(source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
-    psphotAddWithTest (source, true, maskVal); // replace source if subtracted
+    bool subtracted = (source->tmpFlags & PM_SOURCE_TMPF_SUBTRACTED);
+    if (subtracted) pmSourceAdd (source, PM_MODEL_OP_FULL, maskVal);
 
     int nPts = source->pixels->numRows * source->pixels->numCols;
@@ -1240,12 +1151,12 @@
         for (int ix = 0; ix < source->pixels->numCols; ix++) {
             if (source->maskObj->data.PS_TYPE_IMAGE_MASK_DATA[iy][ix]) {
-                // rb->data.F32[nb] = hypot (ix + 0.5 - Xo, iy + 0.5 - Yo) ;
-                rb->data.F32[nb] = hypot (ix - Xo, iy - Yo) ;
+                rb->data.F32[nb] = hypot (ix + 0.5 - Xo, iy + 0.5 - Yo) ;
+                // rb->data.F32[nb] = hypot (ix - Xo, iy - Yo) ;
                 Rb->data.F32[nb] = log10(rb->data.F32[nb]);
                 fb->data.F32[nb] = log10(source->pixels->data.F32[iy][ix]);
                 nb++;
             } else {
-                // rg->data.F32[ng] = hypot (ix + 0.5 - Xo, iy + 0.5 - Yo) ;
-                rg->data.F32[ng] = hypot (ix - Xo, iy - Yo) ;
+                rg->data.F32[ng] = hypot (ix + 0.5 - Xo, iy + 0.5 - Yo) ;
+                // rg->data.F32[ng] = hypot (ix - Xo, iy - Yo) ;
                 Rg->data.F32[ng] = log10(rg->data.F32[ng]);
                 fg->data.F32[ng] = log10(source->pixels->data.F32[iy][ix]);
@@ -1255,6 +1166,66 @@
     }
 
+    // generate model profiles (major and minor axis):
+    // create a model with theta = 0.0 so major and minor axes are equiv to x and y:
+    psEllipseShape rawShape, rotShape;
+
+    rawShape.sx  = source->modelPSF->params->data.F32[PM_PAR_SXX] / M_SQRT2;
+    rawShape.sy  = source->modelPSF->params->data.F32[PM_PAR_SYY] / M_SQRT2;
+    rawShape.sxy = source->modelPSF->params->data.F32[PM_PAR_SXY];
+
+    psEllipseAxes axes = psEllipseShapeToAxes (rawShape, 20.0);
+
+    axes.theta = 0.0;
+
+    rotShape = psEllipseAxesToShape (axes);
+
+    psVector *params = psVectorAlloc(source->modelPSF->params->n, PS_TYPE_F32);
+    for (int i = 0; i < source->modelPSF->params->n; i++) {
+	params->data.F32[i] = source->modelPSF->params->data.F32[i];
+    }
+    params->data.F32[PM_PAR_SXX] = rotShape.sx * M_SQRT2;
+    params->data.F32[PM_PAR_SYY] = rotShape.sy * M_SQRT2;
+    params->data.F32[PM_PAR_SXY] = rotShape.sxy;
+    params->data.F32[PM_PAR_XPOS] = 0.0;
+    params->data.F32[PM_PAR_YPOS] = 0.0;
+
+    psVector *rmod = psVectorAlloc(300, PS_TYPE_F32);
+    psVector *fmaj = psVectorAlloc(300, PS_TYPE_F32);
+    psVector *fmin = psVectorAlloc(300, PS_TYPE_F32);
+
+    psVector *coord = psVectorAlloc(2, PS_TYPE_F32);
+
+    float r = 0.0;
+    for (int i = 0; i < rmod->n; i++) {
+	r = i*0.1;
+	rmod->data.F32[i] = r;
+
+	coord->data.F32[1] = r;
+	coord->data.F32[0] = 0.0;
+	fmaj->data.F32[i] = log10(source->modelPSF->modelFunc (NULL, params, coord));
+
+	coord->data.F32[0] = r;
+	coord->data.F32[1] = 0.0;
+	fmin->data.F32[i] = log10(source->modelPSF->modelFunc (NULL, params, coord));
+    }
+    psFree (coord);
+    psFree (params);
+
+    float FWHM_MAJOR = 2.0*source->modelPSF->modelRadius (source->modelPSF->params, 0.5*source->modelPSF->params->data.F32[PM_PAR_I0]);
+    float FWHM_MINOR = FWHM_MAJOR * (axes.minor / axes.major);
+    if (FWHM_MAJOR < FWHM_MINOR) PS_SWAP (FWHM_MAJOR, FWHM_MINOR); 
+
+    psEllipseMoments emoments;
+    emoments.x2 = source->moments->Mxx;
+    emoments.xy = source->moments->Mxy;
+    emoments.y2 = source->moments->Myy;
+    axes = psEllipseMomentsToAxes (emoments, 20.0);
+    float MOMENTS_MAJOR = 2.355*axes.major;
+    float MOMENTS_MINOR = 2.355*axes.minor;
+
+    float logHM = log10(0.5*source->modelPSF->params->data.F32[PM_PAR_I0]);
+
     // reset source Add/Sub state to recorded
-    psphotSetState (source, state, maskVal);
+    if (subtracted) pmSourceSub (source, PM_MODEL_OP_FULL, maskVal);
 
     KapaInitGraph (&graphdata);
@@ -1291,4 +1262,33 @@
     KapaPlotVector (myKapa, nb, fb->data.F32, "y");
 
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.0;
+    graphdata.style = 0;
+    KapaPrepPlot   (myKapa, rmod->n, &graphdata);
+    KapaPlotVector (myKapa, rmod->n, rmod->data.F32, "x");
+    KapaPlotVector (myKapa, rmod->n, fmin->data.F32, "y");
+    plotline (myKapa, &graphdata, 0.0, logHM, 30.0, logHM);
+    plotline (myKapa, &graphdata, 0.5*FWHM_MINOR, 0.0, 0.5*FWHM_MINOR, 5.0);
+    graphdata.ltype = 1;
+    plotline (myKapa, &graphdata, 0.5*MOMENTS_MINOR, 0.0, 0.5*MOMENTS_MINOR, 5.0);
+    graphdata.ltype = 0;
+	
+    graphdata.color = KapaColorByName ("green");
+    graphdata.ptype = 0;
+    graphdata.size = 0.0;
+    graphdata.style = 0;
+    KapaPrepPlot   (myKapa, rmod->n, &graphdata);
+    KapaPlotVector (myKapa, rmod->n, rmod->data.F32, "x");
+    KapaPlotVector (myKapa, rmod->n, fmaj->data.F32, "y");
+    plotline (myKapa, &graphdata, 0.5*FWHM_MAJOR, 0.0, 0.5*FWHM_MAJOR, 5.0);
+    graphdata.ltype = 1;
+    plotline (myKapa, &graphdata, 0.5*MOMENTS_MAJOR, 0.0, 0.5*MOMENTS_MAJOR, 5.0);
+    graphdata.ltype = 0;
+	
+    for (int i = 0; i < rmod->n; i++) {
+	rmod->data.F32[i] = log10(rmod->data.F32[i]);
+    }
+
     // ** loglog **
     KapaSelectSection (myKapa, "loglog");
@@ -1299,4 +1299,5 @@
     graphdata.ymin = -0.05;
     graphdata.ymax = +5.05;
+    graphdata.color = KapaColorByName ("black");
     KapaSetLimits (myKapa, &graphdata);
 
@@ -1321,4 +1322,24 @@
     KapaPlotVector (myKapa, nb, Rb->data.F32, "x");
     KapaPlotVector (myKapa, nb, fb->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.0;
+    graphdata.style = 0;
+    KapaPrepPlot   (myKapa, rmod->n, &graphdata);
+    KapaPlotVector (myKapa, rmod->n, rmod->data.F32, "x");
+    KapaPlotVector (myKapa, rmod->n, fmin->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("green");
+    graphdata.ptype = 0;
+    graphdata.size = 0.0;
+    graphdata.style = 0;
+    KapaPrepPlot   (myKapa, rmod->n, &graphdata);
+    KapaPlotVector (myKapa, rmod->n, rmod->data.F32, "x");
+    KapaPlotVector (myKapa, rmod->n, fmaj->data.F32, "y");
+
+    psFree (rmod);
+    psFree (fmin);
+    psFree (fmaj);
 
     psFree (rg);
@@ -1337,12 +1358,6 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa3 == -1) {
-        kapa3 = KapaOpenNamedSocket ("kapa", "psphot:plots");
-        if (kapa3 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
+    int myKapa = psphotKapaChannel (2);
+    if (myKapa == -1) return false;
 
     // user-defined masks to test for good/bad pixels (build from recipe list if not yet set)
@@ -1351,5 +1366,7 @@
     assert (maskVal);
 
-    KapaClearPlots (kapa3);
+    section.bg  = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    KapaClearPlots (myKapa);
     // first section : mag vs CR nSigma
     section.dx = 1.0;
@@ -1359,5 +1376,5 @@
     section.name = NULL;
     psStringAppend (&section.name, "linlog");
-    KapaSetSection (kapa3, &section);
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
 
@@ -1369,5 +1386,5 @@
     section.name = NULL;
     psStringAppend (&section.name, "loglog");
-    KapaSetSection (kapa3, &section);
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
 
@@ -1378,5 +1395,5 @@
         if (!(source->mode & PM_SOURCE_MODE_PSFSTAR)) continue;
 
-        psphotVisualPlotRadialProfile (kapa3, source, maskVal);
+        psphotVisualPlotRadialProfile (myKapa, source, maskVal);
 
         // pause and wait for user input:
@@ -1388,5 +1405,5 @@
         }
         if (key[0] == 'e') {
-            KapaClearPlots (kapa3);
+            KapaClearPlots (myKapa);
         }
         if (key[0] == 's') {
@@ -1407,10 +1424,11 @@
     psEllipseAxes axes;
 
+    // XXX skip this for now: it is not very clear
+    return true;
+
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
+    int myKapa = psphotKapaChannel (1);
+    if (myKapa == -1) return false;
 
     // note: this uses the Ohana allocation tools:
@@ -1485,40 +1503,28 @@
     }
 
-    KiiLoadOverlay (kapa, overlayE, NoverlayE, "red");
-    KiiLoadOverlay (kapa, overlayO, NoverlayO, "yellow");
+    KiiLoadOverlay (myKapa, overlayE, NoverlayE, "red");
+    KiiLoadOverlay (myKapa, overlayO, NoverlayO, "yellow");
     FREE (overlayE);
     FREE (overlayO);
 
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
     fprintf (stdout, "even bits (0x0001, 0x0004, ... : red\n");
     fprintf (stdout, "odd bits (0x0002, 0x0008, ... : yellow\n");
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-
-    return true;
-}
-
-bool psphotVisualShowSourceSize (pmReadout *readout, psArray *sources) {
-
-    int Noverlay, NOVERLAY;
+    pmVisualAskUser(NULL);
+
+    return true;
+}
+
+bool psphotVisualShowSourceSize_Single (int myKapa, psArray *sources, pmSourceMode mode, bool keep, float scale, char *color) {
+
+    int Noverlay;
     KiiOverlay *overlay;
 
-    if (!pmVisualIsVisual()) return true;
-
-    if (kapa == -1) {
-        fprintf (stderr, "kapa not opened, skipping\n");
-        return false;
-    }
+    psEllipseMoments emoments;
+    psEllipseAxes axes;
 
     // note: this uses the Ohana allocation tools:
+    ALLOCATE (overlay, KiiOverlay, sources->n);
+
     Noverlay = 0;
-    NOVERLAY = 100;
-    ALLOCATE (overlay, KiiOverlay, sources->n);
-
-    // mark CRs with red boxes
     for (int i = 0; i < sources->n; i++) {
 
@@ -1526,60 +1532,64 @@
         if (source == NULL) continue;
 
-        if (!(source->mode & PM_SOURCE_MODE_CR_LIMIT)) continue;
-
-        overlay[Noverlay].type = KII_OVERLAY_BOX;
-        overlay[Noverlay].x = source->peak->xf;
-        overlay[Noverlay].y = source->peak->yf;
-
-        overlay[Noverlay].dx = 4;
-        overlay[Noverlay].dy = 4;
-        overlay[Noverlay].angle = 0;
+        if (mode) {
+            if (keep) {
+                if (!(source->mode & mode)) continue;
+            } else {
+                if (source->mode & mode) continue;
+            }
+        }
+
+        pmMoments *moments = source->moments;
+        if (moments == NULL) continue;
+
+        overlay[Noverlay].type = KII_OVERLAY_CIRCLE;
+        overlay[Noverlay].x = moments->Mx;
+        overlay[Noverlay].y = moments->My;
+
+        emoments.x2 = moments->Mxx;
+        emoments.y2 = moments->Myy;
+        emoments.xy = moments->Mxy;
+
+        axes = psEllipseMomentsToAxes (emoments, 20.0);
+
+        overlay[Noverlay].dx = scale*2.0*axes.major;
+        overlay[Noverlay].dy = scale*2.0*axes.minor;
+        overlay[Noverlay].angle = axes.theta * PS_DEG_RAD;
         overlay[Noverlay].text = NULL;
         Noverlay ++;
-        CHECK_REALLOCATE (overlay, KiiOverlay, NOVERLAY, Noverlay, 100);
-    }
-    KiiLoadOverlay (kapa, overlay, Noverlay, "red");
-
-
-    Noverlay = 0;
-    for (int i = 0; i < sources->n; i++) {
-
-        pmSource *source = sources->data[i];
-        if (source == NULL) continue;
-
-        // mark EXTs with yellow circles
-        if (!(source->mode & PM_SOURCE_MODE_EXT_LIMIT)) continue;
-
-        overlay[Noverlay].type = KII_OVERLAY_CIRCLE;
-        overlay[Noverlay].x = source->peak->xf;
-        overlay[Noverlay].y = source->peak->yf;
-
-        overlay[Noverlay].dx = 10;
-        overlay[Noverlay].dy = 10;
-        overlay[Noverlay].angle = 0;
-        overlay[Noverlay].text = NULL;
-        Noverlay ++;
-        CHECK_REALLOCATE (overlay, KiiOverlay, NOVERLAY, Noverlay, 100);
-    }
-
-    KiiLoadOverlay (kapa, overlay, Noverlay, "blue");
+    }
+
+    KiiLoadOverlay (myKapa, overlay, Noverlay, color);
     FREE (overlay);
 
-    psphotVisualShowMask (kapa, readout->mask, "mask", 2);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "CR: 4pix red BOX; EXT: 10pix blue circle\n");
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-
-    return true;
-}
-
-bool psphotVisualPlotSourceSize (psArray *sources) {
-
+    return true;
+}
+
+bool psphotVisualShowSourceSize (pmReadout *readout, psArray *sources) {
+
+    if (!pmVisualIsVisual()) return true;
+
+    int myKapa = psphotKapaChannel (1);
+    if (myKapa == -1) return false;
+
+    KiiEraseOverlay (myKapa, "red");
+    KiiEraseOverlay (myKapa, "blue");
+    KiiEraseOverlay (myKapa, "green");
+    KiiEraseOverlay (myKapa, "yellow");
+
+    psphotVisualShowSourceSize_Single (myKapa, sources, PM_SOURCE_MODE_EXT_LIMIT | PM_SOURCE_MODE_DEFECT | PM_SOURCE_MODE_CR_LIMIT | PM_SOURCE_MODE_SATSTAR, 0, 1.0, "green");
+    psphotVisualShowSourceSize_Single (myKapa, sources, PM_SOURCE_MODE_EXT_LIMIT, 1, 1.0, "blue");
+    psphotVisualShowSourceSize_Single (myKapa, sources, PM_SOURCE_MODE_CR_LIMIT, 1, 1.0, "red");
+    psphotVisualShowSourceSize_Single (myKapa, sources, PM_SOURCE_MODE_DEFECT, 1, 2.0, "red");
+    psphotVisualShowSourceSize_Single (myKapa, sources, PM_SOURCE_MODE_SATSTAR, 1, 1.0, "yellow");
+
+    fprintf (stdout, "red: CR; blue: EXTENDED; green: PSF-like; yellow: SATSTAR\n");
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualPlotSourceSize (psMetadata *recipe, psMetadata *analysis, psArray *sources) {
+
+    bool status;
     Graphdata graphdata;
     KapaSection section;
@@ -1587,185 +1597,540 @@
     if (!pmVisualIsVisual()) return true;
 
-    if (kapa3 == -1) {
-        kapa3 = KapaOpenNamedSocket ("kapa", "psphot:plots");
-        if (kapa3 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    KapaClearPlots (kapa3);
+    int myKapa = psphotKapaChannel (2);
+    if (myKapa == -1) return false;
+
+    KapaClearPlots (myKapa);
     KapaInitGraph (&graphdata);
-
-    // first section : mag vs CR nSigma
-    section.dx = 1.0;
-    section.dy = 0.5;
-    section.x = 0.0;
-    section.y = 0.0;
-    section.name = NULL;
-    psStringAppend (&section.name, "a1");
-    KapaSetSection (kapa3, &section);
+    KapaSetFont (myKapa, "courier", 14);
+
+    section.bg  = KapaColorByName ("none"); // XXX probably should be 'none'
+
+    // select the max psfX,Y values for the plot limits
+    float Xmin = 1000.0, Xmax = 0.0;
+    float Ymin = 1000.0, Ymax = 0.0;
+    {
+        int nRegions = psMetadataLookupS32 (&status, analysis, "PSF.CLUMP.NREGIONS");
+        for (int n = 0; n < nRegions; n++) {
+
+            char regionName[64];
+            snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", n);
+            psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
+
+            float psfX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
+            float psfY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");
+            float psfdX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");
+            float psfdY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");
+
+            float X0 = psfX - 10.0*psfdX;
+            float X1 = psfX + 10.0*psfdX;
+            float Y0 = psfY - 10.0*psfdY;
+            float Y1 = psfY + 10.0*psfdY;
+
+            if (isfinite(X0)) { Xmin = PS_MIN(Xmin, X0); }
+            if (isfinite(X1)) { Xmax = PS_MAX(Xmax, X1); }
+            if (isfinite(Y0)) { Ymin = PS_MIN(Ymin, Y0); }
+            if (isfinite(Y1)) { Ymax = PS_MAX(Ymax, Y1); }
+        }
+    }
+    Xmin = PS_MAX(Xmin, -0.1);
+    Ymin = PS_MAX(Ymin, -0.1);
+
+    // storage vectors for data to be plotted
+    psVector *xSAT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *ySAT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mSAT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sSAT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    psVector *xPSF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yPSF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mPSF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sPSF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    psVector *xEXT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yEXT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mEXT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sEXT = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    psVector *xDEF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yDEF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mDEF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sDEF = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    psVector *xLOW = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yLOW = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mLOW = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sLOW = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    psVector *xCR = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *yCR = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *mCR = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *sCR = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    // construct the vectors
+    int nSAT = 0;
+    int nEXT = 0;
+    int nPSF = 0;
+    int nDEF = 0;
+    int nLOW = 0;
+    int nCR  = 0;
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (source->moments == NULL) continue;
+
+	// only plot the measured sources...
+        if (!(source->tmpFlags & PM_SOURCE_TMPF_SIZE_MEASURED)) continue;
+
+        if (source->mode & PM_SOURCE_MODE_CR_LIMIT) {
+            xCR->data.F32[nCR] = source->moments->Mxx;
+            yCR->data.F32[nCR] = source->moments->Myy;
+            mCR->data.F32[nCR] = -2.5*log10(source->moments->Sum);
+            sCR->data.F32[nCR] = source->extNsigma;
+            nCR++;
+        }
+        if (source->mode & PM_SOURCE_MODE_SATSTAR) {
+            xSAT->data.F32[nSAT] = source->moments->Mxx;
+            ySAT->data.F32[nSAT] = source->moments->Myy;
+            mSAT->data.F32[nSAT] = -2.5*log10(source->moments->Sum);
+            sSAT->data.F32[nSAT] = source->extNsigma;
+            nSAT++;
+        }
+        if (source->mode & PM_SOURCE_MODE_EXT_LIMIT) {
+            xEXT->data.F32[nEXT] = source->moments->Mxx;
+            yEXT->data.F32[nEXT] = source->moments->Myy;
+            mEXT->data.F32[nEXT] = -2.5*log10(source->moments->Sum);
+            sEXT->data.F32[nEXT] = source->extNsigma;
+            nEXT++;
+            continue;
+        }
+        if (source->mode & PM_SOURCE_MODE_DEFECT) {
+            xDEF->data.F32[nDEF] = source->moments->Mxx;
+            yDEF->data.F32[nDEF] = source->moments->Myy;
+            mDEF->data.F32[nDEF] = -2.5*log10(source->moments->Sum);
+            sDEF->data.F32[nDEF] = source->extNsigma;
+            nDEF++;
+            continue;
+        }
+        if (source->errMag > 0.1) {
+            xLOW->data.F32[nLOW] = source->moments->Mxx;
+            yLOW->data.F32[nLOW] = source->moments->Myy;
+            mLOW->data.F32[nLOW] = -2.5*log10(source->moments->Sum);
+            sLOW->data.F32[nLOW] = source->extNsigma;
+            nLOW++;
+            continue;
+        }
+        xPSF->data.F32[nPSF] = source->moments->Mxx;
+        yPSF->data.F32[nPSF] = source->moments->Myy;
+        mPSF->data.F32[nPSF] = -2.5*log10(source->moments->Sum);
+        sPSF->data.F32[nPSF] = source->extNsigma;
+        nPSF++;
+    }
+
+    xSAT->n = nSAT;
+    ySAT->n = nSAT;
+    mSAT->n = nSAT;
+    sSAT->n = nSAT;
+
+    xPSF->n = nPSF;
+    yPSF->n = nPSF;
+    mPSF->n = nPSF;
+    sPSF->n = nPSF;
+
+    xEXT->n = nEXT;
+    yEXT->n = nEXT;
+    mEXT->n = nEXT;
+    sEXT->n = nEXT;
+
+    xCR->n = nCR;
+    yCR->n = nCR;
+    mCR->n = nCR;
+    sCR->n = nCR;
+
+    xDEF->n = nDEF;
+    yDEF->n = nDEF;
+    mDEF->n = nDEF;
+    sDEF->n = nDEF;
+
+    xLOW->n = nLOW;
+    yLOW->n = nLOW;
+    mLOW->n = nLOW;
+    sLOW->n = nLOW;
+
+    // four sections: MxxMyy, MagMxx, MagMyy, MagSigma
+
+    // first section: MxxMyy
+    section.dx = 0.75;
+    section.dy = 0.60;
+    section.x  = 0.00;
+    section.y  = 0.00;
+    section.name = psStringCopy ("MxxMyy");
+    KapaSetSection (myKapa, &section);
     psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = Xmin;
+    graphdata.ymin = Ymin;
+    graphdata.xmax = Xmax;
+    graphdata.ymax = Ymax;
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = NAN;
+    graphdata.padYm = NAN;
+    graphdata.padXp = 0.5;
+    graphdata.padYp = 0.5;
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "M_xx| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "M_yy| (pixels)", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nPSF, &graphdata);
+    KapaPlotVector (myKapa, nPSF, xPSF->data.F32, "x");
+    KapaPlotVector (myKapa, nPSF, yPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nEXT, &graphdata);
+    KapaPlotVector (myKapa, nEXT, xEXT->data.F32, "x");
+    KapaPlotVector (myKapa, nEXT, yEXT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nDEF, &graphdata);
+    KapaPlotVector (myKapa, nDEF, xDEF->data.F32, "x");
+    KapaPlotVector (myKapa, nDEF, yDEF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nCR, &graphdata);
+    KapaPlotVector (myKapa, nCR, xCR->data.F32, "x");
+    KapaPlotVector (myKapa, nCR, yCR->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nSAT, &graphdata);
+    KapaPlotVector (myKapa, nSAT, xSAT->data.F32, "x");
+    KapaPlotVector (myKapa, nSAT, ySAT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nLOW, &graphdata);
+    KapaPlotVector (myKapa, nLOW, xLOW->data.F32, "x");
+    KapaPlotVector (myKapa, nLOW, yLOW->data.F32, "y");
+
+    // second section: MagMyy
+    section.dx = 0.75;
+    section.dy = 0.20;
+    section.x  = 0.00;
+    section.y  = 0.80;
+    section.name = psStringCopy ("MagMyy");
+    KapaSetSection (myKapa, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = -17.1;
+    graphdata.xmax =  -6.9;
+    graphdata.ymin = Ymin;
+    graphdata.ymax = Ymax;
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = 0.5;
+    graphdata.padYm = NAN;
+    graphdata.padXp = NAN;
+    graphdata.padYp = 0.5;
+    strcpy (graphdata.labels, "0210");
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "inst mag", KAPA_LABEL_XP);
+    KapaSendLabel (myKapa, "M_yy| (pixels)", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nPSF, &graphdata);
+    KapaPlotVector (myKapa, nPSF, mPSF->data.F32, "x");
+    KapaPlotVector (myKapa, nPSF, yPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nEXT, &graphdata);
+    KapaPlotVector (myKapa, nEXT, mEXT->data.F32, "x");
+    KapaPlotVector (myKapa, nEXT, yEXT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nDEF, &graphdata);
+    KapaPlotVector (myKapa, nDEF, mDEF->data.F32, "x");
+    KapaPlotVector (myKapa, nDEF, yDEF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nCR, &graphdata);
+    KapaPlotVector (myKapa, nCR, mCR->data.F32, "x");
+    KapaPlotVector (myKapa, nCR, yCR->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nSAT, &graphdata);
+    KapaPlotVector (myKapa, nSAT, mSAT->data.F32, "x");
+    KapaPlotVector (myKapa, nSAT, ySAT->data.F32, "y");
+
+    // third section: MagMxx
+    section.dx = 0.25;
+    section.dy = 0.60;
+    section.x  = 0.75;
+    section.y  = 0.00;
+    section.name = psStringCopy ("MagMxx");
+    KapaSetSection (myKapa, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmin = Xmin;
+    graphdata.xmax = Xmax;
+    graphdata.ymin =  -6.9;
+    graphdata.ymax = -17.1;
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = NAN;
+    graphdata.padYm = 0.5;
+    graphdata.padXp = 0.5;
+    graphdata.padYp = NAN;
+    strcpy (graphdata.labels, "2001");
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "M_xx| (pixels)", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "inst mag", KAPA_LABEL_YP);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nPSF, &graphdata);
+    KapaPlotVector (myKapa, nPSF, xPSF->data.F32, "x");
+    KapaPlotVector (myKapa, nPSF, mPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nEXT, &graphdata);
+    KapaPlotVector (myKapa, nEXT, xEXT->data.F32, "x");
+    KapaPlotVector (myKapa, nEXT, mEXT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nDEF, &graphdata);
+    KapaPlotVector (myKapa, nDEF, xDEF->data.F32, "x");
+    KapaPlotVector (myKapa, nDEF, mDEF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nCR, &graphdata);
+    KapaPlotVector (myKapa, nCR, xCR->data.F32, "x");
+    KapaPlotVector (myKapa, nCR, mCR->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nSAT, &graphdata);
+    KapaPlotVector (myKapa, nSAT, xSAT->data.F32, "x");
+    KapaPlotVector (myKapa, nSAT, mSAT->data.F32, "y");
+
+    // fourth section: MagSigma
+    section.dx = 0.75;
+    section.dy = 0.20;
+    section.x  = 0.00;
+    section.y  = 0.60;
+    section.name = psStringCopy ("MagSigma");
+    KapaSetSection (myKapa, &section);
+    psFree (section.name);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.xmax =  -6.9;
+    graphdata.xmin = -17.1;
+    graphdata.ymin = -20.1;
+    graphdata.ymax = +20.1;
+    KapaSetLimits (myKapa, &graphdata);
+
+    graphdata.padXm = 0.5;
+    graphdata.padYm = NAN;
+    graphdata.padXp = 0.5;
+    graphdata.padYp = 0.5;
+    strcpy (graphdata.labels, "0100");
+    KapaBox (myKapa, &graphdata);
+    // KapaSendLabel (myKapa, "inst mag", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "EXT&ss&c", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nPSF, &graphdata);
+    KapaPlotVector (myKapa, nPSF, mPSF->data.F32, "x");
+    KapaPlotVector (myKapa, nPSF, sPSF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nEXT, &graphdata);
+    KapaPlotVector (myKapa, nEXT, mEXT->data.F32, "x");
+    KapaPlotVector (myKapa, nEXT, sEXT->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nDEF, &graphdata);
+    KapaPlotVector (myKapa, nDEF, mDEF->data.F32, "x");
+    KapaPlotVector (myKapa, nDEF, sDEF->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nCR, &graphdata);
+    KapaPlotVector (myKapa, nCR, mCR->data.F32, "x");
+    KapaPlotVector (myKapa, nCR, sCR->data.F32, "y");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 7;
+    graphdata.size = 1.0;
+    graphdata.style = 2;
+    KapaPrepPlot   (myKapa, nSAT, &graphdata);
+    KapaPlotVector (myKapa, nSAT, mSAT->data.F32, "x");
+    KapaPlotVector (myKapa, nSAT, sSAT->data.F32, "y");
+
+    // draw N circles to outline the clumps
+    {
+        KapaSelectSection (myKapa, "MxxMyy");
+
+        // draw a circle centered on psfX,Y with size of the psf limit
+        psVector *xLimit  = psVectorAlloc (120, PS_TYPE_F32);
+        psVector *yLimit  = psVectorAlloc (120, PS_TYPE_F32);
+
+        int nRegions = psMetadataLookupS32 (&status, analysis, "PSF.CLUMP.NREGIONS");
+        float PSF_CLUMP_NSIGMA = psMetadataLookupF32 (&status, recipe, "PSF_CLUMP_NSIGMA");
+
+        graphdata.color = KapaColorByName ("blue");
+        graphdata.style = 0;
+
+        graphdata.xmin = Xmin;
+        graphdata.ymin = Ymin;
+        graphdata.xmax = Xmax;
+        graphdata.ymax = Ymax;
+        KapaSetLimits (myKapa, &graphdata);
+
+        for (int n = 0; n < nRegions; n++) {
+
+            char regionName[64];
+            snprintf (regionName, 64, "PSF.CLUMP.REGION.%03d", n);
+            psMetadata *regionMD = psMetadataLookupPtr (&status, analysis, regionName);
+
+            float psfX  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.X");
+            float psfY  = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.Y");
+            float psfdX = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DX");
+            float psfdY = psMetadataLookupF32 (&status, regionMD, "PSF.CLUMP.DY");
+            float Rx = psfdX * PSF_CLUMP_NSIGMA;
+            float Ry = psfdY * PSF_CLUMP_NSIGMA;
+
+            for (int i = 0; i < xLimit->n; i++) {
+                xLimit->data.F32[i] = Rx*cos(i*2.0*M_PI/120.0) + psfX;
+                yLimit->data.F32[i] = Ry*sin(i*2.0*M_PI/120.0) + psfY;
+            }
+            KapaPrepPlot (myKapa, xLimit->n, &graphdata);
+            KapaPlotVector (myKapa, xLimit->n, xLimit->data.F32, "x");
+            KapaPlotVector (myKapa, yLimit->n, yLimit->data.F32, "y");
+        }
+        psFree (xLimit);
+        psFree (yLimit);
+    }
+
+    psFree (xSAT);
+    psFree (ySAT);
+    psFree (mSAT);
+    psFree (sSAT);
+
+    psFree (xEXT);
+    psFree (yEXT);
+    psFree (mEXT);
+    psFree (sEXT);
+
+    psFree (xPSF);
+    psFree (yPSF);
+    psFree (mPSF);
+    psFree (sPSF);
+
+    psFree (xDEF);
+    psFree (yDEF);
+    psFree (mDEF);
+    psFree (sDEF);
+
+    psFree (xLOW);
+    psFree (yLOW);
+    psFree (mLOW);
+    psFree (sLOW);
+
+    psFree (xCR);
+    psFree (yCR);
+    psFree (mCR);
+    psFree (sCR);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualShowResidualImage (pmReadout *readout) {
+
+    if (!pmVisualIsVisual()) return true;
+
+    int myKapa = psphotKapaChannel (1);
+    if (myKapa == -1) return false;
+
+    psphotVisualScaleImage (myKapa, readout->image, readout->mask, "resid", 1);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualPlotApResid (psArray *sources, float mean, float error) {
+
+    Graphdata graphdata;
+    float lineX[2], lineY[2];
+
+    if (!pmVisualIsVisual()) return true;
+
+    int myKapa = psphotKapaChannel (2);
+    if (myKapa == -1) return false;
+
+    KapaClearPlots (myKapa);
+    KapaInitGraph (&graphdata);
 
     psVector *x = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
     psVector *y = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
-
-    graphdata.xmin = +32.0;
-    graphdata.xmax = -32.0;
-    graphdata.ymin = +32.0;
-    graphdata.ymax = -32.0;
-
-    // construct the plot vectors
-    int n = 0;
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-        if (!source) continue;
-        if (source->type != PM_SOURCE_TYPE_STAR) continue;
-        if (!isfinite (source->crNsigma)) continue;
-
-        x->data.F32[n] = -2.5*log10(source->peak->flux);
-        y->data.F32[n] = source->crNsigma;
-        graphdata.xmin = PS_MIN(graphdata.xmin, x->data.F32[n]);
-        graphdata.xmax = PS_MAX(graphdata.xmax, x->data.F32[n]);
-        graphdata.ymin = -0.5;
-        graphdata.ymax = 10.0;
-
-        n++;
-    }
-    x->n = y->n = n;
-
-    float range;
-    range = graphdata.xmax - graphdata.xmin;
-    graphdata.xmax += 0.05*range;
-    graphdata.xmin -= 0.05*range;
-
-    // XXX set the plot range to match the image
-    KapaSetLimits (kapa3, &graphdata);
-
-    KapaSetFont (kapa3, "helvetica", 14);
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "Peak as Mag", KAPA_LABEL_XM);
-    KapaSendLabel (kapa3, "CR N Sigma", KAPA_LABEL_YM);
-
-    graphdata.color = KapaColorByName ("black");
-    graphdata.ptype = 2;
-    graphdata.size = 0.5;
-    graphdata.style = 2;
-    KapaPrepPlot (kapa3, n, &graphdata);
-    KapaPlotVector (kapa3, n, x->data.F32, "x");
-    KapaPlotVector (kapa3, n, y->data.F32, "y");
-
-    // second section : mag vs EXT nSigma
-    section.dx = 1.0;
-    section.dy = 0.5;
-    section.x = 0.0;
-    section.y = 0.5;
-    section.name = NULL;
-    psStringAppend (&section.name, "a2");
-    KapaSetSection (kapa3, &section);
-    psFree (section.name);
-
-    graphdata.xmin = +32.0;
-    graphdata.xmax = -32.0;
-    graphdata.ymin = +32.0;
-    graphdata.ymax = -32.0;
-
-    // construct the plot vectors
-    n = 0;
-    for (int i = 0; i < sources->n; i++) {
-        pmSource *source = sources->data[i];
-        if (!source) continue;
-        if (source->type != PM_SOURCE_TYPE_STAR) continue;
-        if (!isfinite (source->extNsigma)) continue;
-
-        x->data.F32[n] = -2.5*log10(source->peak->flux);
-        y->data.F32[n] = source->extNsigma;
-        graphdata.xmin = PS_MIN(graphdata.xmin, x->data.F32[n]);
-        graphdata.xmax = PS_MAX(graphdata.xmax, x->data.F32[n]);
-        graphdata.ymin = -0.5;
-        graphdata.ymax = 10.0;
-
-        n++;
-    }
-    x->n = y->n = n;
-
-    range = graphdata.xmax - graphdata.xmin;
-    graphdata.xmax += 0.05*range;
-    graphdata.xmin -= 0.05*range;
-
-    // XXX set the plot range to match the image
-    KapaSetLimits (kapa3, &graphdata);
-
-    KapaSetFont (kapa3, "helvetica", 14);
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "EXT N Sigma", KAPA_LABEL_YM);
-
-    graphdata.color = KapaColorByName ("black");
-    graphdata.ptype = 2;
-    graphdata.size = 0.5;
-    graphdata.style = 2;
-    KapaPrepPlot (kapa3, n, &graphdata);
-    KapaPlotVector (kapa3, n, x->data.F32, "x");
-    KapaPlotVector (kapa3, n, y->data.F32, "y");
-
-    psFree (x);
-    psFree (y);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-    return true;
-}
-
-bool psphotVisualShowResidualImage (pmReadout *readout) {
-
-    if (!pmVisualIsVisual()) return true;
-
-    if (kapa == -1) {
-        kapa = KapaOpenNamedSocket ("kapa", "psphot:images");
-        if (kapa == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    psphotVisualScaleImage (kapa, readout->image, "resid", 1);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
-    return true;
-}
-
-bool psphotVisualPlotApResid (psArray *sources) {
-
-    Graphdata graphdata;
-
-    if (!pmVisualIsVisual()) return true;
-
-    if (kapa3 == -1) {
-        kapa3 = KapaOpenNamedSocket ("kapa", "psphot:plots");
-        if (kapa3 == -1) {
-            fprintf (stderr, "failure to open kapa; visual mode disabled\n");
-            pmVisualSetVisual(false);
-            return false;
-        }
-    }
-
-    KapaClearPlots (kapa3);
-    KapaInitGraph (&graphdata);
-
-    psVector *x = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
-    psVector *y = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *dy = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
 
     graphdata.xmin = +32.0;
@@ -1785,4 +2150,5 @@
         x->data.F32[n] = source->psfMag;
         y->data.F32[n] = source->apMag - source->psfMag;
+        dy->data.F32[n] = source->errMag;
         graphdata.xmin = PS_MIN(graphdata.xmin, x->data.F32[n]);
         graphdata.xmax = PS_MAX(graphdata.xmax, x->data.F32[n]);
@@ -1792,5 +2158,5 @@
         n++;
     }
-    x->n = y->n = n;
+    x->n = y->n = dy->n = n;
 
     float range;
@@ -1802,11 +2168,16 @@
     graphdata.ymin -= 0.05*range;
 
-    // XXX set the plot range to match the image
-    KapaSetLimits (kapa3, &graphdata);
-
-    KapaSetFont (kapa3, "helvetica", 14);
-    KapaBox (kapa3, &graphdata);
-    KapaSendLabel (kapa3, "PSF Mag", KAPA_LABEL_XM);
-    KapaSendLabel (kapa3, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
+    // XXX test
+    graphdata.xmin = -17.0;
+    graphdata.xmax =  -9.0;
+    graphdata.ymin = -0.31;
+    graphdata.ymax = +0.31;
+
+    KapaSetLimits (myKapa, &graphdata);
+
+    KapaSetFont (myKapa, "helvetica", 14);
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "Ap Mag - PSF Mag", KAPA_LABEL_YM);
 
     graphdata.color = KapaColorByName ("black");
@@ -1814,18 +2185,187 @@
     graphdata.size = 0.5;
     graphdata.style = 2;
-    KapaPrepPlot (kapa3, n, &graphdata);
-    KapaPlotVector (kapa3, n, x->data.F32, "x");
-    KapaPlotVector (kapa3, n, y->data.F32, "y");
+    graphdata.etype |= 0x01;
+    KapaPrepPlot (myKapa, n, &graphdata);
+    KapaPlotVector (myKapa, n, x->data.F32, "x");
+    KapaPlotVector (myKapa, n, y->data.F32, "y");
+    KapaPlotVector (myKapa, n, dy->data.F32, "dym");
+    KapaPlotVector (myKapa, n, dy->data.F32, "dyp");
+
+    graphdata.color = KapaColorByName ("blue");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 0;
+    graphdata.etype = 0;
+    lineX[0] = graphdata.xmin;
+    lineX[1] = graphdata.xmax;
+    lineY[0] = lineY[1] = mean;
+    KapaPrepPlot (myKapa, 2, &graphdata);
+    KapaPlotVector (myKapa, 2, lineX, "x");
+    KapaPlotVector (myKapa, 2, lineY, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 0;
+    graphdata.etype = 0;
+    lineX[0] = graphdata.xmin;
+    lineX[1] = graphdata.xmax;
+    lineY[0] = lineY[1] = mean + error;
+    KapaPrepPlot (myKapa, 2, &graphdata);
+    KapaPlotVector (myKapa, 2, lineX, "x");
+    KapaPlotVector (myKapa, 2, lineY, "y");
+
+    graphdata.color = KapaColorByName ("red");
+    graphdata.ptype = 0;
+    graphdata.size = 0.5;
+    graphdata.style = 0;
+    graphdata.etype = 0;
+    lineX[0] = graphdata.xmin;
+    lineX[1] = graphdata.xmax;
+    lineY[0] = lineY[1] = mean - error;
+    KapaPrepPlot (myKapa, 2, &graphdata);
+    KapaPlotVector (myKapa, 2, lineX, "x");
+    KapaPlotVector (myKapa, 2, lineY, "y");
 
     psFree (x);
     psFree (y);
-
-    // pause and wait for user input:
-    // continue, save (provide name), ??
-    char key[10];
-    fprintf (stdout, "[c]ontinue? ");
-    if (!fgets(key, 8, stdin)) {
-        psWarning("Unable to read option");
-    }
+    psFree (dy);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualPlotChisq (psArray *sources) {
+
+    Graphdata graphdata;
+
+    if (!pmVisualIsVisual()) return true;
+
+    int myKapa = psphotKapaChannel (2);
+    if (myKapa == -1) return false;
+
+    KapaClearPlots (myKapa);
+    KapaInitGraph (&graphdata);
+
+    psVector *x = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+    psVector *y = psVectorAllocEmpty (sources->n, PS_TYPE_F32);
+
+    graphdata.xmin = +32.0;
+    graphdata.xmax = -32.0;
+    graphdata.ymin = +32.0;
+    graphdata.ymax = -32.0;
+
+    FILE *f = fopen ("chisq.dat", "w");
+
+    // construct the plot vectors
+    int n = 0;
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+        if (!source) continue;
+        if (source->type != PM_SOURCE_TYPE_STAR) continue;
+        if (!source->moments) continue;
+        if (!isfinite(source->moments->Sum)) continue;
+        if (!source->modelPSF) continue;
+        if (!isfinite(source->modelPSF->chisq)) continue;
+
+        x->data.F32[n] = -2.5*log10(source->moments->Sum);
+        y->data.F32[n] = source->modelPSF->chisq / source->modelPSF->nDOF;
+        graphdata.xmin = PS_MIN(graphdata.xmin, x->data.F32[n]);
+        graphdata.xmax = PS_MAX(graphdata.xmax, x->data.F32[n]);
+        graphdata.ymin = PS_MIN(graphdata.ymin, y->data.F32[n]);
+        graphdata.ymax = PS_MAX(graphdata.ymax, y->data.F32[n]);
+
+        fprintf (f, "%d %d %f %f\n", i, n, x->data.F32[n], y->data.F32[n]);
+
+        n++;
+    }
+    x->n = y->n = n;
+    fclose (f);
+
+    float range;
+    range = graphdata.xmax - graphdata.xmin;
+    graphdata.xmax += 0.05*range;
+    graphdata.xmin -= 0.05*range;
+    range = graphdata.ymax - graphdata.ymin;
+    graphdata.ymax += 0.05*range;
+    graphdata.ymin -= 0.05*range;
+
+    // XXX test
+    graphdata.xmin = -17.0;
+    graphdata.xmax =  -3.0;
+    graphdata.ymin =  -0.1;
+    graphdata.ymax = +10.1;
+
+    KapaSetLimits (myKapa, &graphdata);
+
+    KapaSetFont (myKapa, "helvetica", 14);
+    KapaBox (myKapa, &graphdata);
+    KapaSendLabel (myKapa, "PSF Mag", KAPA_LABEL_XM);
+    KapaSendLabel (myKapa, "ChiSq", KAPA_LABEL_YM);
+
+    graphdata.color = KapaColorByName ("black");
+    graphdata.ptype = 2;
+    graphdata.size = 0.5;
+    graphdata.style = 2;
+    KapaPrepPlot (myKapa, n, &graphdata);
+    KapaPlotVector (myKapa, n, x->data.F32, "x");
+    KapaPlotVector (myKapa, n, y->data.F32, "y");
+
+    psFree (x);
+    psFree (y);
+
+    pmVisualAskUser(NULL);
+    return true;
+}
+
+bool psphotVisualShowPetrosians (psArray *sources) {
+
+    int Noverlay, NOVERLAY;
+    KiiOverlay *overlay;
+
+    if (!pmVisualIsVisual()) return true;
+
+    int kapa = psphotKapaChannel (1);
+    if (kapa == -1) return false;
+
+    Noverlay = 0;
+    NOVERLAY = 100;
+    ALLOCATE (overlay, KiiOverlay, NOVERLAY);
+
+    for (int i = 0; i < sources->n; i++) {
+        pmSource *source = sources->data[i];
+
+        if (!source) continue;
+        if (!source->extpars) continue;
+        if (!source->extpars->petProfile) continue;
+
+        float petrosianRadius = source->extpars->petrosianRadius;
+	psEllipseAxes *axes = &source->extpars->axes;
+
+        overlay[Noverlay].type = KII_OVERLAY_CIRCLE;
+        overlay[Noverlay].x = source->peak->xf;
+        overlay[Noverlay].y = source->peak->yf;
+        overlay[Noverlay].dx = 1.0*petrosianRadius;
+        overlay[Noverlay].dy = 1.0*petrosianRadius*axes->minor/axes->major;
+        overlay[Noverlay].angle = axes->theta * PS_DEG_RAD;
+        overlay[Noverlay].text = NULL;
+        Noverlay ++;
+        CHECK_REALLOCATE (overlay, KiiOverlay, NOVERLAY, Noverlay, 100);
+
+        // overlay[Noverlay].type = KII_OVERLAY_CIRCLE;
+        // overlay[Noverlay].x = source->peak->xf;
+        // overlay[Noverlay].y = source->peak->yf;
+        // overlay[Noverlay].dx = 2.0*petrosianRadius;
+        // overlay[Noverlay].dy = 2.0*petrosianRadius*axes->minor/axes->major;
+        // overlay[Noverlay].angle = axes->theta * PS_DEG_RAD;
+        // overlay[Noverlay].text = NULL;
+        // Noverlay ++;
+        // CHECK_REALLOCATE (overlay, KiiOverlay, NOVERLAY, Noverlay, 100);
+    }
+
+    KiiLoadOverlay (kapa, overlay, Noverlay, "red");
+    FREE (overlay);
+
+    pmVisualAskUser(NULL);
     return true;
 }
@@ -1850,2 +2390,40 @@
 
 # endif
+
+# if (0)
+// *** make a histogram of the source counts in the x and y directions
+psHistogram *nX = psHistogramAlloc (graphdata.xmin, graphdata.xmax, 50.0);
+psHistogram *nY = psHistogramAlloc (graphdata.ymin, graphdata.ymax, 50.0);
+psVectorHistogram (nX, xFaint, NULL, NULL, 0);
+psVectorHistogram (nY, yFaint, NULL, NULL, 0);
+psVector *dX = psVectorAlloc (nX->nums->n, PS_TYPE_F32);
+psVector *vX = psVectorAlloc (nX->nums->n, PS_TYPE_F32);
+psVector *dY = psVectorAlloc (nY->nums->n, PS_TYPE_F32);
+psVector *vY = psVectorAlloc (nY->nums->n, PS_TYPE_F32);
+for (int i = 0; i < nX->nums->n; i++) {
+    dX->data.F32[i] = nX->nums->data.S32[i];
+    vX->data.F32[i] = 0.5*(nX->bounds->data.F32[i] + nX->bounds->data.F32[i+1]);
+}
+for (int i = 0; i < nY->nums->n; i++) {
+    dY->data.F32[i] = nY->nums->data.S32[i];
+    vY->data.F32[i] = 0.5*(nY->bounds->data.F32[i] + nY->bounds->data.F32[i+1]);
+}
+
+graphdata.color = KapaColorByName ("black");
+graphdata.ptype = 0;
+graphdata.size = 0.0;
+graphdata.style = 0;
+KapaPrepPlot (myKapa, dX->n, &graphdata);
+KapaPlotVector (myKapa, dX->n, dX->data.F32, "x");
+KapaPlotVector (myKapa, vX->n, vX->data.F32, "y");
+
+psFree (nX);
+psFree (dX);
+psFree (vX);
+
+psFree (nY);
+psFree (dY);
+psFree (vY);
+
+# endif
+
